diff --git a/.github/ISSUE_TEMPLATE/1_bug.yml b/.github/ISSUE_TEMPLATE/1_bug.yml new file mode 100644 index 00000000..b4c597dc --- /dev/null +++ b/.github/ISSUE_TEMPLATE/1_bug.yml @@ -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: | +
Code sample + + ```dart + [Paste your code here] + ``` + +
+ validations: + required: true + - type: textarea + attributes: + label: Screenshots or Video + description: | + Upload any screenshots or video of the bug if applicable. + value: | +
+ Screenshots / Video demonstration + + [Upload media here] + +
+ 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: | +
Logs + + ```console + [Paste your logs here] + ``` + +
+ validations: + required: false \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/2_feature_request.yml b/.github/ISSUE_TEMPLATE/2_feature_request.yml new file mode 100644 index 00000000..26100b3d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/2_feature_request.yml @@ -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 \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/3_question.yml b/.github/ISSUE_TEMPLATE/3_question.yml new file mode 100644 index 00000000..509ae635 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/3_question.yml @@ -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 \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/issue-template.md b/.github/ISSUE_TEMPLATE/issue-template.md deleted file mode 100644 index c8a389aa..00000000 --- a/.github/ISSUE_TEMPLATE/issue-template.md +++ /dev/null @@ -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 latest flutter version in stable channel on branch master. If you are using beta or master channel, or you are not using latest flutter version in stable channel, you may experience error. - - - - - - - - - \ No newline at end of file diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index c4810500..c62f265d 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,30 +1,20 @@ -# Pull Request - ## Description -Provide a brief description of your changes. +*Replace this paragraph with a description of what this PR is doing. If you're modifying existing behavior, describe the existing behavior, how this PR is changing it, and what motivated the change.* -## Issues +## Related Issues - -Closes #IssueNumber -(Replace "IssueNumber" with the actual issue number you are addressing.) +*Replace this paragraph with a list of issues related to this PR from the [issue database](https://github.com/singerdmx/flutter-quill/issues). Indicate, which of these issues are resolved or fixed by this PR.* -## Improvements - +*e.g.* +- *Fix #123* +- *Related #456* - -- Improve code readability -- Improve performance +## Improvements + ## Features - - - -- Add a new feature -- Allow to customize the widgets - - + ## Additional notes @@ -34,13 +24,15 @@ Closes #IssueNumber ## Checklist - +- [ ] I read the [Contributor Guide](../CONTRIBUTING.md) and followed the process outlined there for submitting PRs. +- [ ] I titled the PR using [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0). +- [ ] I did not modify the `CHANGELOG.md` nor the plugin version in `pubspec.yaml` files. +- [ ] All existing and new tests are passing. +- [ ] I have run the commands in `./scripts/before-push.sh` and it all passed successfully + +## Breaking Change + +Does your PR require plugin users to manually update their apps to accommodate your change? -- [ ] I have added/updated relevant documentation -- [ ] I have tested these changes locally. -- [ ] I have followed the code style and guidelines. -- [ ] I have updated `CHANGELOG.md` with my changes in the next section -- [ ] I have run `dart format .`` on the project -- [ ] I have run `dart fix --apply` on the project -- [ ] I have run `flutter test` and `flutter analyze` and it passed successfully -- [ ] I have run `./before-push.sh` and everything is fine +- [ ] Yes, this is a breaking change (please indicate that with a `!` in the title as explained in [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0)). +- [ ] No, this is *not* a breaking change. \ No newline at end of file diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index c5314cc0..aa72b67b 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,4 +1,4 @@ -name: flutter-quill CI +name: Flutter Quill CI on: push: @@ -11,10 +11,11 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: subosito/flutter-action@v2 with: channel: 'stable' + cache: true - name: Check flutter version run: flutter --version @@ -25,14 +26,34 @@ jobs: - name: Install flutter_quill_extensions dependencies run: flutter pub get -C flutter_quill_extensions + - name: Install flutter_quill_test dependencies + run: flutter pub get -C flutter_quill_test + + - name: Install quill_html_converter dependencies + run: flutter pub get -C packages/quill_html_converter + - name: Run flutter analysis run: flutter analyze - name: Check dart code formatting run: dart format --set-exit-if-changed . + + - name: Preview dart proposed changes + run: dart fix --dry-run - name: Check if package is ready for publishing run: flutter pub publish --dry-run - name: Run flutter tests run: flutter test + + - name: Flutter build Web + run: flutter build web --release --verbose --dart-define=CI=true + working-directory: ./example + + # - name: Install flutter Linux prerequisites + # run: sudo apt-get install clang cmake git ninja-build pkg-config libgtk-3-dev liblzma-dev libstdc++-12-dev -y + + # - name: Flutter build Linux + # run: flutter build linux --release --verbose --dart-define=CI=true + # working-directory: ./example diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 00000000..644aac7d --- /dev/null +++ b/.github/workflows/publish.yml @@ -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 diff --git a/.github/workflows/welcome.yml b/.github/workflows/welcome.yml new file mode 100644 index 00000000..54027d81 --- /dev/null +++ b/.github/workflows/welcome.yml @@ -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!!' \ No newline at end of file diff --git a/.gitignore b/.gitignore index 6b87759f..cee99ed0 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ *.pyc *.swp .DS_Store +**/.DS_Store .atom/ .buildlog/ .history @@ -75,3 +76,8 @@ example/ios/Podfile.lock !**/ios/**/default.pbxuser !**/ios/**/default.perspectivev3 pubspec.lock + +# For local development +pubspec_overrides.yaml + +old_example \ No newline at end of file diff --git a/.pubignore b/.pubignore new file mode 100644 index 00000000..e4e6f2b1 --- /dev/null +++ b/.pubignore @@ -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/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 5776f039..5e024d5d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,549 +1,441 @@ -## [8.1.1] -- Fix null error in line.dart [#1487](https://github.com/singerdmx/flutter-quill/issues/1487) +# Changelog -## [8.1.0] -- Fixes a word typo of `mirgration` to `migration` in readme & migration document. -- Updated migration guide -- Remove property `enableUnfocusOnTapOutside` in QuillEditor Configurations and add `isOnTapOutsideEnabled` instead -- Add a new callback which is called `onTapOutside` in the `QuillEditorConfigurations` which allow you to do something when tap outside of the edtior -- Fix a bug which cause the web platform to not unfocus the editor when tap outside of it (the default logic) to override this pleae pass a value to the callback ``onTapOutside`` -- Remove the old proerty of `iconTheme`, instead pass `iconTheme` in the button options, you will find `base` property there, inside it there is `iconTheme` +All notable changes to this project will be documented in this file. -## [8.0.0] -- If you have mirgrated recently, don't get scared from this update, it just add a documentation, mirgration guide and mark the version as more stable release, since we did break a lot of breaking changes (at least that what most developers says) we should have change the major version but when we were in the development of this new version, our time was very tight and now we are fixing the version number -- It also rename one single property from `code` to `codeBlock` in the `elements` of the new `QuillEditor` Configurations class -- Updating the README to be more readable +## 8.5.5 +* Now when opening dialogs by `QuillToolbar` you will not get an exception when you don't use `FlutterQuillLocalizations.delegate` in your `WidgetsApp`, `MaterialApp`, or `CupertinoApp`. The fix is for the `QuillToolbarSearchButton`, `QuillToolbarLinkStyleButton`, and `QuillToolbarColorButton` buttons -## [7.10.2] -- Removing line numbers from code block by default, you still can enable this thanks to the new configurations in the `QuillEditor` you will find a `elementOptions` property, in it you will find the code which mean code block options. just pass true to `enableLineNumbers` +## 8.5.4 +* The `mobileWidth`, `mobileHeight`, `mobileMargin` and `mobileAlignment` is now deprecated in `flutter_quill`, they are are now defined in `flutter_quill_extensions` +* Deprecate `replaceStyleStringWithSize` function which is in `string.dart` +* Deprecate `alignment`, and `margin` as they don't conform to official Quill JS -## [7.10.1] -- Fixes and use the new parameters -- You don't need to use MaterialApp anymore to use most of the toolbar buttons childBuilder anymore -- Compatibility with [fresh_quill_extensions](https://pub.dev/packages/fresh_quill_extensions) which is temporary alternative to [flutter_quill_extensions](https://pub.dev/packages/flutter_quill_extensions) -- Finally update most of the documentation in `README.md` +## 8.5.3 +* Update doc +* Update `README.md` and `CHANGELOG.md` +* Fix typos +* Use `immutable` when possible +* Update `.pubignore` -## [7.10.0] -- **Breaking change**: `QuillToolbar.basic()` can be accessed from `QuillToolbar()` directly and the old `QuillToolbar` can be accessed from `QuillBaseToolbar` -- The Quill editor and toolbar configurations are now refactored in one single class for each one -- After changing one of the checkbox list values the controller will not request the keyboard focus by default -- We have moved the configurations of the toolbar and the editor directly into the widget but we are still using inherited widgets internally -- Fixes to some of the code after the refactoring +## 8.5.2 +* Updated `README.md`. +* Feature: Added the ability to include a custom callback when the `QuillToolbarColorButton` is pressed. +* The `QuillToolbar` now implements `PreferredSizeWidget`, enabling usage in the AppBar, similar to `QuillBaseToolbar`. -## [7.9.0] -- Buttons Improvemenets -- Refactor all the button configurations that used in `QuillToolbar.basic()` but there are still few lefts -- **Breaking change**: Remove some configurations from the QuillToolbar and move them to the new `QuillProvider`, please notice this is a development version and this might be changed in the next few days, the stable release will be ready in less than 3 weeks -- Update `flutter_quill_extensions` and it will be published into pub.dev soon. -- Allow you to customize the search dialog by custom callback with child builder +## 8.5.1 +* Updated `README.md`. -## [7.8.0] -- **Important note**: this is not test release yet, it works but need more test and changes and breaking changes, we don't have development version and it will help us if you try the latest version and report the issues in Github but if you want a stable version please use `7.4.16`. this refactoring process will not take long and should be done less than three weeks with the testing. -- We managed to refactor most of the buttons configurations and customizations in the `QuillProvider`, only three lefts then will start on refactoring the toolbar configurations -- Code improvemenets +## 8.5.0 +* Migrated to `flutter_localizations` for translations. +* Fixed: Translated all previously untranslated localizations. +* Fixed: Added translations for missing items. +* Fixed: Introduced default Chinese fallback translation. +* Removed: Unused parameters `items` in `QuillToolbarFontFamilyButtonOptions` and `QuillToolbarFontSizeButtonOptions`. +* Updated: Documentation. -## [7.7.0] +## 8.4.4 +* Updated `.pubignore` to ignore unnecessary files and folders. -- **Breaking change**: We have mirgrated more buttons in the toolbar configurations, you can do change them in the `QuillProvider` -- Important bug fixes +## 8.4.3 +* Updated `CHANGELOG.md`. -## [7.6.1] +## 8.4.2 +* **Breaking change**: Configuration for `QuillRawEditor` has been moved to a separate class. Additionally, `readOnly` has been renamed to `isReadOnly`. If using `QuillEditor`, no action is required. +* Introduced the ability for developers to override `TextInputAction` in both `QuillRawEditor` and `QuillEditor`. +* Enabled using `QuillRawEditor` without `QuillEditorProvider`. +* Bug fixes. +* Added image cropping implementation in the example. + +## 8.4.1 +* Added `copyWith` in `OptionalSize` class. -- Bug fixes +## 8.4.0 +* **Breaking change**: Updated `QuillCustomButton` to use `QuillCustomButtonOptions`. Moved all properties from `QuillCustomButton` to `QuillCustomButtonOptions`, replacing `iconData` with `icon` widget for increased customization. +* **Breaking change**: `customButtons` in `QuillToolbarConfigurations` is now of type `List`. +* Bug fixes following the `8.0.0` update. +* Updated `README.md`. +* Improved platform checking. -## [7.6.0] +## 8.3.0 +* Added `iconButtonFactor` property to `QuillToolbarBaseButtonOptions` for customizing button size relative to its icon size (defaults to `kIconButtonFactor`, consistent with previous releases). -- **Breaking change**: To customize the buttons in the toolbar, you can do that in the `QuillProvider` +## 8.2.6 +* Organized `QuillRawEditor` code. -# [7.5.0] +## 8.2.5 +* Added `builder` property in `QuillEditorConfigurations`. -- **Breaking change**: The widgets `QuillEditor` and `QuillToolbar` are no longer have controller parameter, instead you need to make sure in the widget tree you have wrapped them with `QuillProvider` widget and provide the controller and the require configurations +## 8.2.4 +* Adhered to Flutter best practices. +* Fixed auto-focus bug. -# [7.4.16] +## 8.2.3 +* Updated `README.md`. -- Update documentation and README.md +## 8.2.2 +* Moved `flutter_quill_test` to a separate package: [flutter_quill_test](https://pub.dev/packages/flutter_quill_test). -# [7.4.15] +## 8.2.1 +* Updated `README.md`. -- Custom style attrbuites for platforms other than mobile (alignment, margin, width, height) -- Bug fixes and other improvemenets +## 8.2.0 +* Added the option to add configurations for `flutter_quill_extensions` using `extraConfigurations`. -# [7.4.14] +## 8.1.11 +* Followed Dart best practices by using `lints` and removed `pedantic` and `platform` since they are not used. +* Fixed text direction bug. +* Updated `README.md`. -- Improve performance by reducing the number of widgets rebuilt by listening to media query for only the needed things, for example instead of using `MediaQuery.of(context).size`, now we are using `MediaQuery.sizeOf(context)` -- Add MediaButton for picking the images only since the video one is not ready -- A new feature which allows customizing the text selection in quill editor which is useful for custom theme design system for custom app widget +## 8.1.10 +* Secret for automated publishing to pub.dev. -# [7.4.13] +## 8.1.9 +* Fixed automated publishing to pub.dev. -- Fixed tab editing when in readOnly mode. +## 8.1.8 +* Fixed automated publishing to pub.dev. -# [7.4.12] +## 8.1.7 +* Automated publishing to pub.dev. -- Update the minimum version of device_info_plus to 9.1.0. +## 8.1.6 +* Fixed compatibility with `integration_test` by downgrading the minimum version of the platform package to 3.1.0. -# [7.4.11] +## 8.1.5 +* Reversed background/font color toolbar button icons. -- Add sw locale. +## 8.1.4 +* Reversed background/font color toolbar button tooltips. -# [7.4.10] +## 8.1.3 +* Moved images to screenshots instead of `README.md`. -- Update translations. +## 8.1.2 +* Fixed a bug related to the regexp of the insert link dialog. +* Required Dart 3 as the minimum version. +* Code cleanup. +* Added a spacer widget between each button in the `QuillToolbar`. -# [7.4.9] +## 8.1.1 +* Fixed null error in line.dart #1487(https://github.com/singerdmx/flutter*quill/issues/1487). -- Style recognition fixes. +## 8.1.0 +* Fixed a word typo of `mirgration` to `migration` in the readme & migration document. +* Updated migration guide. +* Removed property `enableUnfocusOnTapOutside` in `QuillEditor` configurations and added `isOnTapOutsideEnabled` instead. +* Added a new callback called `onTapOutside` in the `QuillEditorConfigurations` to perform actions when tapping outside the editor. +* Fixed a bug that caused the web platform to not unfocus the editor when tapping outside of it. To override this, please pass a value to the `onTapOutside` callback. +* Removed the old property of `iconTheme`. Instead, pass `iconTheme` in the button options; you will find the `base` property inside it with `iconTheme`. -# [7.4.8] +## 8.0.0 +* If you have migrated recently, don't be alarmed by this update; it adds documentation, a migration guide, and marks the version as a more stable release. Although there are breaking changes (as reported by some developers), the major version was not changed due to time constraints during development. A single property was also renamed from `code` to `codeBlock` in the `elements` of the new `QuillEditorConfigurations` class. +* Updated the README for better readability. -- Upgrade dependencies. +## 7.10.2 +* Removed line numbers from code blocks by default. You can still enable this feature thanks to the new configurations in the `QuillEditor`. Find the `elementOptions` property and enable `enableLineNumbers`. -# [7.4.7] +## 7.10.1 +* Fixed issues and utilized the new parameters. +* No longer need to use `MaterialApp` for most toolbar button child builders. +* Compatibility with [fresh_quill_extensions](https://pub.dev/packages/fresh_quill_extensions), a temporary alternative to [flutter_quill_extensions](https://pub.dev/packages/flutter_quill_extensions). +* Updated most of the documentation in `README.md`. -- Add Vietnamese and German translations. +## 7.10.0 +* **Breaking change**: `QuillToolbar.basic()` can be accessed directly from `QuillToolbar()`, and the old `QuillToolbar` can be accessed from `QuillBaseToolbar`. +* Refactored Quill editor and toolbar configurations into a single class each. +* After changing checkbox list values, the controller will not request keyboard focus by default. +* Moved toolbar and editor configurations directly into the widget but still use inherited widgets internally. +* Fixes to some code after the refactoring. -# [7.4.6] +## 7.1.14 -- Fix more null errors in Leaf.retain [#1394](https://github.com/singerdmx/flutter-quill/issues/1394) and Line.delete [#1395](https://github.com/singerdmx/flutter-quill/issues/1395). +* Add indents change for multiline selection. -# [7.4.5] +## 7.1.13 -- Fix null error in Container.insert [#1392](https://github.com/singerdmx/flutter-quill/issues/1392). +* Add custom recognizer. -# [7.4.4] +## 7.1.12 -- Fix extra padding on checklists [#1131](https://github.com/singerdmx/flutter-quill/issues/1131). +* Add superscript and subscript styles. -# [7.4.3] +## 7.1.11 -- Fixed a space input error on iPad. +* Add inserting indents for lines of list if text is selected. -# [7.4.2] +## 7.1.10 -- Fix bug with keepStyleOnNewLine for link. +* Image embedding tweaks + * Add MediaButton which is intened to superseed the ImageButton and VideoButton. Only image selection is working. + * Implement image insert for web (image as base64) -# [7.4.1] +## 7.1.9 -- Fix toolbar dividers condition. +* Editor tweaks PR from bambinoua(https://github.com/bambinoua). + * Shortcuts now working in Mac OS + * QuillDialogTheme is extended with new properties buttonStyle, linkDialogConstraints, imageDialogConstraints, isWrappable, runSpacing, + * Added LinkStyleButton2 with new LinkStyleDialog (similar to Quill implementation + * Conditinally use Row or Wrap for dialog's children. + * Update minimum Dart SDK version to 2.17.0 to use enum extensions. + * Use merging shortcuts and actions correclty (if the key combination is the same) -# [7.4.0] +## 7.1.8 -- Support Flutter version 3.13.0. +* Dropdown tweaks + * Add itemHeight, itemPadding, defaultItemColor for customization of dropdown items. + * Remove alignment property as useless. + * Fix bugs with max width when width property is null. -# [7.3.3] +## 7.1.7 -- Updated Dependencies conflicting. - -# [7.3.2] - -- Added builder for custom button in _LinkDialog. - -# [7.3.1] - -- Added case sensitive and whole word search parameters. -- Added wrap around. -- Moved search dialog to the bottom in order not to override the editor and the text found. -- Other minor search dialog enhancements. - -# [7.3.0] - -- Add default attributes to basic factory. - -# [7.2.19] - -- Feat/link regexp. - -# [7.2.18] - -- Fix paste block text in words apply same style. - -# [7.2.17] - -- Fix paste text mess up style. -- Add support copy/cut block text. - -# [7.2.16] - -- Allow for custom context menu. - -# [7.2.15] - -- Add flutter_quill.delta library which only exposes Delta datatype. - -# [7.2.14] - -- Fix errors when the editor is used in the `screenshot` package. - -# [7.2.13] - -- Fix around image can't delete line break. - -# [7.2.12] - -- Add support for copy/cut select image and text together. - -# [7.2.11] - -- Add affinity for localPosition. - -# [7.2.10] - -- LINE._getPlainText queryChild inclusive=false. - -# [7.2.9] - -- Add toPlainText method to `EmbedBuilder`. - -# [7.2.8] - -- Add custom button widget in toolbar. - -# [7.2.7] - -- Fix language code of Japan. - -# [7.2.6] - -- Style custom toolbar buttons like builtins. - -# [7.2.5] - -- Always use text cursor for editor on desktop. - -# [7.2.4] - -- Fixed keepStyleOnNewLine. - -# [7.2.3] - -- Get pixel ratio from view. - -# [7.2.2] - -- Prevent operations on stale editor state. - -# [7.2.1] - -- Add support for android keyboard content insertion. -- Enhance color picker, enter hex color and color palette option. - -# [7.2.0] - -- Checkboxes, bullet points, and number points are now scaled based on the default paragraph font size. - -# [7.1.20] - -- Pass linestyle to embedded block. - -# [7.1.19] - -- Fix Rtl leading alignment problem. - -# [7.1.18] - -- Support flutter latest version. - -# [7.1.17+1] - -- Updates `device_info_plus` to version 9.0.0 to benefit from AGP 8 (see [changelog#900](https://pub.dev/packages/device_info_plus/changelog#900)). - -# [7.1.16] - -- Fixed subscript key from 'sup' to 'sub'. - -# [7.1.15] - -- Fixed a bug introduced in 7.1.7 where each section in `QuillToolbar` was displayed on its own line. - -# [7.1.14] - -- Add indents change for multiline selection. - -# [7.1.13] - -- Add custom recognizer. - -# [7.1.12] - -- Add superscript and subscript styles. - -# [7.1.11] - -- Add inserting indents for lines of list if text is selected. - -# [7.1.10] - -- Image embedding tweaks - - Add MediaButton which is intened to superseed the ImageButton and VideoButton. Only image selection is working. - - Implement image insert for web (image as base64) - -# [7.1.9] - -- Editor tweaks PR from [bambinoua](https://github.com/bambinoua). - - Shortcuts now working in Mac OS - - QuillDialogTheme is extended with new properties buttonStyle, linkDialogConstraints, imageDialogConstraints, isWrappable, runSpacing, - - Added LinkStyleButton2 with new LinkStyleDialog (similar to Quill implementation - - Conditinally use Row or Wrap for dialog's children. - - Update minimum Dart SDK version to 2.17.0 to use enum extensions. - - Use merging shortcuts and actions correclty (if the key combination is the same) - -# [7.1.8] - -- Dropdown tweaks - - Add itemHeight, itemPadding, defaultItemColor for customization of dropdown items. - - Remove alignment property as useless. - - Fix bugs with max width when width property is null. - -# [7.1.7] - -- Toolbar tweaks. - - Implement tooltips for embed CameraButton, VideoButton, FormulaButton, ImageButton. - - Extends customization for SelectAlignmentButton, QuillFontFamilyButton, QuillFontSizeButton adding padding, text style, alignment, width. - - Add renderFontFamilies to QuillFontFamilyButton to show font faces in dropdown. - - Add AxisDivider and its named constructors for for use in parent project. - - Export ToolbarButtons enum to allow specify tooltips for SelectAlignmentButton. - - Export QuillFontFamilyButton, SearchButton as they were not exported before. - - Deprecate items property in QuillFontFamilyButton, QuillFontSizeButton as the it can be built usinr rawItemsMap. - - Make onSelection QuillFontFamilyButton, QuillFontSizeButton omittable as no need to execute callback outside if controller is passed to widget. +* Toolbar tweaks. + * Implement tooltips for embed CameraButton, VideoButton, FormulaButton, ImageButton. + * Extends customization for SelectAlignmentButton, QuillFontFamilyButton, QuillFontSizeButton adding padding, text style, alignment, width. + * Add renderFontFamilies to QuillFontFamilyButton to show font faces in dropdown. + * Add AxisDivider and its named constructors for for use in parent project. + * Export ToolbarButtons enum to allow specify tooltips for SelectAlignmentButton. + * Export QuillFontFamilyButton, SearchButton as they were not exported before. + * Deprecate items property in QuillFontFamilyButton, QuillFontSizeButton as the it can be built usinr rawItemsMap. + * Make onSelection QuillFontFamilyButton, QuillFontSizeButton omittable as no need to execute callback outside if controller is passed to widget. Now the package is more friendly for web projects. -# [7.1.6] +## 7.1.6 -- Add enableUnfocusOnTapOutside field to RawEditor and Editor widgets. +* Add enableUnfocusOnTapOutside field to RawEditor and Editor widgets. -# [7.1.5] +## 7.1.5 -- Add tooltips for toolbar buttons. +* Add tooltips for toolbar buttons. -# [7.1.4] +## 7.1.4 -- Fix inserting tab character in lists. +* Fix inserting tab character in lists. -# [7.1.3] +## 7.1.3 -- Fix ios cursor bug when word.length==1. +* Fix ios cursor bug when word.length==1. -# [7.1.2] +## 7.1.2 -- Fix non scrollable editor exception, when tapped under content. +* Fix non scrollable editor exception, when tapped under content. -# [7.1.1] +## 7.1.1 -- customLinkPrefixes parameter - makes possible to open links with custom protoco. +* customLinkPrefixes parameter * makes possible to open links with custom protoco. -# [7.1.0] +## 7.1.0 -- Fix ordered list numeration with several lists in document. +* Fix ordered list numeration with several lists in document. -# [7.0.9] +## 7.0.9 -- Use const constructor for EmbedBuilder. +* Use const constructor for EmbedBuilder. -# [7.0.8] +## 7.0.8 -- Fix IME position bug with scroller. +* Fix IME position bug with scroller. -# [7.0.7] +## 7.0.7 -- Add TextFieldTapRegion for contextMenu. +* Add TextFieldTapRegion for contextMenu. -# [7.0.6] +## 7.0.6 -- Fix line style loss on new line from non string. +* Fix line style loss on new line from non string. -# [7.0.5] +## 7.0.5 -- Fix IME position bug for Mac and Windows. -- Unfocus when tap outside editor. fix the bug that cant refocus in afterButtonPressed after click ToggleStyleButton on Mac. +* Fix IME position bug for Mac and Windows. +* Unfocus when tap outside editor. fix the bug that cant refocus in afterButtonPressed after click ToggleStyleButton on Mac. -# [7.0.4] +## 7.0.4 -- Have text selection span full line height for uneven sized text. +* Have text selection span full line height for uneven sized text. -# [7.0.3] +## 7.0.3 -- Fix ordered list numeration for lists with more than one level of list. +* Fix ordered list numeration for lists with more than one level of list. -# [7.0.2] +## 7.0.2 -- Allow widgets to override widget span properties. +* Allow widgets to override widget span properties. -# [7.0.1] +## 7.0.1 -- Update i18n_extension dependency to version 8.0.0. +* Update i18n_extension dependency to version 8.0.0. -# [7.0.0] +## 7.0.0 -- Breaking change: Tuples are no longer used. They have been replaced with a number of data classes. +* Breaking change: Tuples are no longer used. They have been replaced with a number of data classes. -# [6.4.4] +## 6.4.4 -- Increased compatibility with Flutter widget tests. +* Increased compatibility with Flutter widget tests. -# [6.4.3] +## 6.4.3 -- Update dependencies (collection: 1.17.0, flutter_keyboard_visibility: 5.4.0, quiver: 3.2.1, tuple: 2.0.1, url_launcher: 6.1.9, characters: 1.2.1, i18n_extension: 7.0.0, device_info_plus: 8.1.0) +* Update dependencies (collection: 1.17.0, flutter_keyboard_visibility: 5.4.0, quiver: 3.2.1, tuple: 2.0.1, url_launcher: 6.1.9, characters: 1.2.1, i18n_extension: 7.0.0, device_info_plus: 8.1.0) -# [6.4.2] +## 6.4.2 -- Replace `buildToolbar` with `contextMenuBuilder`. +* Replace `buildToolbar` with `contextMenuBuilder`. -# [6.4.1] +## 6.4.1 -- Control the detect word boundary behaviour. +* Control the detect word boundary behaviour. -# [6.4.0] +## 6.4.0 -- Use `axis` to make the toolbar vertical. -- Use `toolbarIconCrossAlignment` to align the toolbar icons on the cross axis. -- Breaking change: `QuillToolbar`'s parameter `toolbarHeight` was renamed to `toolbarSize`. +* Use `axis` to make the toolbar vertical. +* Use `toolbarIconCrossAlignment` to align the toolbar icons on the cross axis. +* Breaking change: `QuillToolbar`'s parameter `toolbarHeight` was renamed to `toolbarSize`. -# [6.3.5] +## 6.3.5 -- Ability to add custom shortcuts. +* Ability to add custom shortcuts. -# [6.3.4] +## 6.3.4 -- Update clipboard status prior to showing selected text overlay. +* Update clipboard status prior to showing selected text overlay. -# [6.3.3] +## 6.3.3 -- Fixed handling of mac intents. +* Fixed handling of mac intents. -# [6.3.2] +## 6.3.2 -- Added `unknownEmbedBuilder` to QuillEditor. -- Fix error style when input chinese japanese or korean. +* Added `unknownEmbedBuilder` to QuillEditor. +* Fix error style when input chinese japanese or korean. -# [6.3.1] +## 6.3.1 -- Add color property to the basic factory function. +* Add color property to the basic factory function. -# [6.3.0] +## 6.3.0 -- Support Flutter 3.7. +* Support Flutter 3.7. -# [6.2.2] +## 6.2.2 -- Fix: nextLine getter null where no assertion. +* Fix: nextLine getter null where no assertion. -# [6.2.1] +## 6.2.1 -- Revert "Align numerical and bullet lists along with text content". +* Revert "Align numerical and bullet lists along with text content". -# [6.2.0] +## 6.2.0 -- Align numerical and bullet lists along with text content. +* Align numerical and bullet lists along with text content. -# [6.1.12] +## 6.1.12 -- Apply i18n for default font dropdown option labels corresponding to 'Clear'. +* Apply i18n for default font dropdown option labels corresponding to 'Clear'. -# [6.1.11] +## 6.1.11 -- Remove iOS hack for delaying focus calculation. +* Remove iOS hack for delaying focus calculation. -# [6.1.10] +## 6.1.10 -- Delay focus calculation for iOS. +* Delay focus calculation for iOS. -# [6.1.9] +## 6.1.9 -- Bump keyboard show up wait to 1 sec. +* Bump keyboard show up wait to 1 sec. -# [6.1.8] +## 6.1.8 -- Recalculate focus when showing keyboard. +* Recalculate focus when showing keyboard. -# [6.1.7] +## 6.1.7 -- Add czech localizations. +* Add czech localizations. -# [6.1.6] +## 6.1.6 -- Upgrade i18n_extension to 6.0.0. +* Upgrade i18n_extension to 6.0.0. -# [6.1.5] +## 6.1.5 -- Fix formatting exception. +* Fix formatting exception. -# [6.1.4] +## 6.1.4 -- Add double quotes validation. +* Add double quotes validation. -# [6.1.3] +## 6.1.3 -- Revert "fix order list numbering (#988)". +* Revert "fix order list numbering (##988)". -# [6.1.2] +## 6.1.2 -- Add typing shortcuts. +* Add typing shortcuts. -# [6.1.1] +## 6.1.1 -- Fix order list numbering. +* Fix order list numbering. -# [6.1.0] +## 6.1.0 -- Add keyboard shortcuts for editor actions. +* Add keyboard shortcuts for editor actions. -# [6.0.10] +## 6.0.10 -- Upgrade device info plus to ^7.0.0. +* Upgrade device info plus to ^7.0.0. -# [6.0.9] +## 6.0.9 -- Don't throw showAutocorrectionPromptRect not implemented. The function is called with every keystroke as a user is typing. +* Don't throw showAutocorrectionPromptRect not implemented. The function is called with every keystroke as a user is typing. -# [6.0.8+1] +## 6.0.8+1 -- Fixes null pointer when setting documents. +* Fixes null pointer when setting documents. -# [6.0.8] +## 6.0.8 -- Make QuillController.document mutable. +* Make QuillController.document mutable. -# [6.0.7] +## 6.0.7 -- Allow disabling of selection toolbar. +* Allow disabling of selection toolbar. -# [6.0.6+1] +## 6.0.6+1 -- Revert 6.0.6. +* Revert 6.0.6. -# [6.0.6] +## 6.0.6 -- Fix wrong custom embed key. +* Fix wrong custom embed key. -# [6.0.5] +## 6.0.5 -- Fixes toolbar buttons stealing focus from editor. +* Fixes toolbar buttons stealing focus from editor. -# [6.0.4] +## 6.0.4 -- Bug fix for Type 'Uint8List' not found. +* Bug fix for Type 'Uint8List' not found. -# [6.0.3] +## 6.0.3 -- Add ability to paste images. +* Add ability to paste images. -# [6.0.2] +## 6.0.2 -- Address Dart Analysis issues. +* Address Dart Analysis issues. -# [6.0.1] +## 6.0.1 -- Changed translation country code (zh_HK -> zh_hk) to lower case, which is required for i18n_extension used in flutter_quill. -- Add localization in example's main to demonstrate translation. -- Issue [Windows] selection's copy / paste tool bar not shown #861: add selection's copy / paste toolbar, escape to hide toolbar, mouse right click to show toolbar, ctrl-Y / ctrl-Z to undo / redo. -- Image and video displayed in Windows platform caused screen flickering while selecting text, a sample_data_nomedia.json asset is added for Desktop to demonstrate the added features. -- Known issue: keyboard action sometimes causes exception mentioned in Flutter's issue #106475 ([Windows] Keyboard shortcuts stop working after modifier key repeat flutter/flutter#106475). -- Know issue: user needs to click the editor to get focus before toolbar is able to display. +* Changed translation country code (zh_HK -> zh_hk) to lower case, which is required for i18n_extension used in flutter_quill. +* Add localization in example's main to demonstrate translation. +* Issue Windows selection's copy / paste tool bar not shown ##861: add selection's copy / paste toolbar, escape to hide toolbar, mouse right click to show toolbar, ctrl-Y / ctrl-Z to undo / redo. +* Image and video displayed in Windows platform caused screen flickering while selecting text, a sample_data_nomedia.json asset is added for Desktop to demonstrate the added features. +* Known issue: keyboard action sometimes causes exception mentioned in Flutter's issue ##106475 (Windows Keyboard shortcuts stop working after modifier key repeat flutter/flutter##106475). +* Know issue: user needs to click the editor to get focus before toolbar is able to display. -# [6.0.0] BREAKING CHANGE +## 6.0.0 BREAKING CHANGE -- Removed embed (image, video & formula) blocks from the package to reduce app size. +* Removed embed (image, video & formula) blocks from the package to reduce app size. These blocks have been moved to the package `flutter_quill_extensions`, migrate by filling the `embedBuilders` and `embedButtons` parameters as follows: @@ -561,929 +453,929 @@ QuillToolbar.basic( ); ``` -# [5.4.2] +## 5.4.2 -- Upgrade i18n_extension. +* Upgrade i18n_extension. -# [5.4.1] +## 5.4.1 -- Update German Translation. +* Update German Translation. -# [5.4.0] +## 5.4.0 -- Added Formula Button (for maths support). +* Added Formula Button (for maths support). -# [5.3.2] +## 5.3.2 -- Add more font family. +* Add more font family. -# [5.3.1] +## 5.3.1 -- Enable search when text is not empty. +* Enable search when text is not empty. -# [5.3.0] +## 5.3.0 -- Added search function. +* Added search function. -# [5.2.11] +## 5.2.11 -- Remove default small color. +* Remove default small color. -# [5.2.10] +## 5.2.10 -- Don't wrap the QuillEditor's child in the EditorTextSelectionGestureDetector if selection is disabled. +* Don't wrap the QuillEditor's child in the EditorTextSelectionGestureDetector if selection is disabled. -# [5.2.9] +## 5.2.9 -- Added option to modify SelectHeaderStyleButton options. -- Added option to click again on h1, h2, h3 button to go back to normal. +* Added option to modify SelectHeaderStyleButton options. +* Added option to click again on h1, h2, h3 button to go back to normal. -# [5.2.8] +## 5.2.8 -- Remove tooltip for LinkStyleButton. -- Make link match regex case insensitive. +* Remove tooltip for LinkStyleButton. +* Make link match regex case insensitive. -# [5.2.7] +## 5.2.7 -- Add locale to QuillEditor.basic. +* Add locale to QuillEditor.basic. -# [5.2.6] +## 5.2.6 -- Fix keyboard pops up when resizing the image. +* Fix keyboard pops up when resizing the image. -# [5.2.5] +## 5.2.5 -- Upgrade youtube_player_flutter_quill to 8.2.2. +* Upgrade youtube_player_flutter_quill to 8.2.2. -# [5.2.4] +## 5.2.4 -- Upgrade youtube_player_flutter_quill to 8.2.1. +* Upgrade youtube_player_flutter_quill to 8.2.1. -# [5.2.3] +## 5.2.3 -- Flutter Quill Doesn't Work On iOS 16 or Xcode 14 Betas (Stored properties cannot be marked potentially unavailable with '@available'). +* Flutter Quill Doesn't Work On iOS 16 or Xcode 14 Betas (Stored properties cannot be marked potentially unavailable with '@available'). -# [5.2.2] +## 5.2.2 -- Fix Web Unsupported operation: Platform.\_operatingSystem error. +* Fix Web Unsupported operation: Platform.\_operatingSystem error. -# [5.2.1] +## 5.2.1 -- Rename QuillCustomIcon to QuillCustomButton. +* Rename QuillCustomIcon to QuillCustomButton. -# [5.2.0] +## 5.2.0 -- Support font family selection. +* Support font family selection. -# [5.1.1] +## 5.1.1 -- Update README. +* Update README. -# [5.1.0] +## 5.1.0 -- Added CustomBlockEmbed and customElementsEmbedBuilder. +* Added CustomBlockEmbed and customElementsEmbedBuilder. -# [5.0.5] +## 5.0.5 -- Upgrade device_info_plus to 4.0.0. +* Upgrade device_info_plus to 4.0.0. -# [5.0.4] +## 5.0.4 -- Added onVideoInit callback for video documents. +* Added onVideoInit callback for video documents. -# [5.0.3] +## 5.0.3 -- Update dependencies. +* Update dependencies. -# [5.0.2] +## 5.0.2 -- Keep cursor position on checkbox tap. +* Keep cursor position on checkbox tap. -# [5.0.1] +## 5.0.1 -- Fix static analysis errors. +* Fix static analysis errors. -# [5.0.0] +## 5.0.0 -- Flutter 3.0.0 support. +* Flutter 3.0.0 support. -# [4.2.3] +## 4.2.3 -- Ignore color:inherit and convert double to int for level. +* Ignore color:inherit and convert double to int for level. -# [4.2.2] +## 4.2.2 -- Add clear option to font size dropdown. +* Add clear option to font size dropdown. -# [4.2.1] +## 4.2.1 -- Refactor font size dropdown. +* Refactor font size dropdown. -# [4.2.0] +## 4.2.0 -- Ensure selectionOverlay is available for showToolbar. +* Ensure selectionOverlay is available for showToolbar. -# [4.1.9] +## 4.1.9 -- Using properly iconTheme colors. +* Using properly iconTheme colors. -# [4.1.8] +## 4.1.8 -- Update font size dropdown. +* Update font size dropdown. -# [4.1.7] +## 4.1.7 -- Convert FontSize to a Map to allow for named Font Size. +* Convert FontSize to a Map to allow for named Font Size. -# [4.1.6] +## 4.1.6 -- Update quill_dropdown_button.dart. +* Update quill_dropdown_button.dart. -# [4.1.5] +## 4.1.5 -- Add Font Size dropdown to the toolbar. +* Add Font Size dropdown to the toolbar. -# [4.1.4] +## 4.1.4 -- New borderRadius for iconTheme. +* New borderRadius for iconTheme. -# [4.1.3] +## 4.1.3 -- Fix selection handles show/hide after paste, backspace, copy. +* Fix selection handles show/hide after paste, backspace, copy. -# [4.1.2] +## 4.1.2 -- Add full support for hardware keyboards (Chromebook, Android tablets, etc) that don't alter screen UI. +* Add full support for hardware keyboards (Chromebook, Android tablets, etc) that don't alter screen UI. -# [4.1.1] +## 4.1.1 -- Added textSelectionControls field in QuillEditor. +* Added textSelectionControls field in QuillEditor. -# [4.1.0] +## 4.1.0 -- Added Node to linkActionPickerDelegate. +* Added Node to linkActionPickerDelegate. -# [4.0.12] +## 4.0.12 -- Add Persian(fa) language. +* Add Persian(fa) language. -# [4.0.11] +## 4.0.11 -- Fix cut selection error in multi-node line. +* Fix cut selection error in multi-node line. -# [4.0.10] +## 4.0.10 -- Fix vertical caret position bug. +* Fix vertical caret position bug. -# [4.0.9] +## 4.0.9 -- Request keyboard focus when no child is found. +* Request keyboard focus when no child is found. -# [4.0.8] +## 4.0.8 -- Fix blank lines do not display when --web-renderer=html. +* Fix blank lines do not display when **web*renderer=html. -# [4.0.7] +## 4.0.7 -- Refactor getPlainText (better handling of blank lines and lines with multiple markups. +* Refactor getPlainText (better handling of blank lines and lines with multiple markups. -# [4.0.6] +## 4.0.6 -- Bug fix for copying text with new lines. +* Bug fix for copying text with new lines. -# [4.0.5] +## 4.0.5 -- Fixed casting null to Tuple2 when link dialog is dismissed without any input (e.g. barrier dismissed). +* Fixed casting null to Tuple2 when link dialog is dismissed without any input (e.g. barrier dismissed). -# [4.0.4] +## 4.0.4 -- Bug fix for text direction rtl. +* Bug fix for text direction rtl. -# [4.0.3] +## 4.0.3 -- Support text direction rtl. +* Support text direction rtl. -# [4.0.2] +## 4.0.2 -- Clear toggled style on selection change. +* Clear toggled style on selection change. -# [4.0.1] +## 4.0.1 -- Fix copy/cut/paste/selectAll not working. +* Fix copy/cut/paste/selectAll not working. -# [4.0.0] +## 4.0.0 -- Upgrade for Flutter 2.10. +* Upgrade for Flutter 2.10. -# [3.9.11] +## 3.9.11 -- Added Indonesian translation. +* Added Indonesian translation. -# [3.9.10] +## 3.9.10 -- Fix for undoing a modification ending with an indented line. +* Fix for undoing a modification ending with an indented line. -# [3.9.9] +## 3.9.9 -- iOS: Save image whose filename does not end with image file extension. +* iOS: Save image whose filename does not end with image file extension. -# [3.9.8] +## 3.9.8 -- Added Urdu translation. +* Added Urdu translation. -# [3.9.7] +## 3.9.7 -- Fix for clicking on the Link button without any text on a new line crashes. +* Fix for clicking on the Link button without any text on a new line crashes. -# [3.9.6] +## 3.9.6 -- Apply locale to QuillEditor(contents). +* Apply locale to QuillEditor(contents). -# [3.9.5] +## 3.9.5 -- Fix image pasting. +* Fix image pasting. -# [3.9.4] +## 3.9.4 -- Hiding dialog after selecting action for image. +* Hiding dialog after selecting action for image. -# [3.9.3] +## 3.9.3 -- Update ImageResizer for Android. +* Update ImageResizer for Android. -# [3.9.2] +## 3.9.2 -- Copy image with its style. +* Copy image with its style. -# [3.9.1] +## 3.9.1 -- Support resizing image. +* Support resizing image. -# [3.9.0] +## 3.9.0 -- Image menu options for copy/remove. +* Image menu options for copy/remove. -# [3.8.8] +## 3.8.8 -- Update set textEditingValue. +* Update set textEditingValue. -# [3.8.7] +## 3.8.7 -- Fix checkbox not toggled correctly in toolbar button. +* Fix checkbox not toggled correctly in toolbar button. -# [3.8.6] +## 3.8.6 -- Fix cursor position changes when checking/unchecking the checkbox. +* Fix cursor position changes when checking/unchecking the checkbox. -# [3.8.5] +## 3.8.5 -- Fix \_handleDragUpdate in \_TextSelectionHandleOverlayState. +* Fix \_handleDragUpdate in \_TextSelectionHandleOverlayState. -# [3.8.4] +## 3.8.4 -- Fix link dialog layout. +* Fix link dialog layout. -# [3.8.3] +## 3.8.3 -- Fix for errors on a non scrollable editor. +* Fix for errors on a non scrollable editor. -# [3.8.2] +## 3.8.2 -- Fix certain keys not working on web when editor is a child of a scroll view. +* Fix certain keys not working on web when editor is a child of a scroll view. -# [3.8.1] +## 3.8.1 -- Refactor \_QuillEditorState to QuillEditorState. +* Refactor \_QuillEditorState to QuillEditorState. -# [3.8.0] +## 3.8.0 -- Support pasting with format. +* Support pasting with format. -# [3.7.3] +## 3.7.3 -- Fix selection overlay for collapsed selection. +* Fix selection overlay for collapsed selection. -# [3.7.2] +## 3.7.2 -- Reverted Embed toPlainText change. +* Reverted Embed toPlainText change. -# [3.7.1] +## 3.7.1 -- Change Embed toPlainText to be empty string. +* Change Embed toPlainText to be empty string. -# [3.7.0] +## 3.7.0 -- Replace Toolbar showHistory group with individual showRedo and showUndo. +* Replace Toolbar showHistory group with individual showRedo and showUndo. -# [3.6.5] +## 3.6.5 -- Update Link dialogue for image/video. +* Update Link dialogue for image/video. -# [3.6.4] +## 3.6.4 -- Link dialogue TextInputType.multiline. +* Link dialogue TextInputType.multiline. -# [3.6.3] +## 3.6.3 -- Bug fix for link button text selection. +* Bug fix for link button text selection. -# [3.6.2] +## 3.6.2 -- Improve link button. +* Improve link button. -# [3.6.1] +## 3.6.1 -- Remove SnackBar 'What is entered is not a link'. +* Remove SnackBar 'What is entered is not a link'. -# [3.6.0] +## 3.6.0 -- Allow link button to enter text. +* Allow link button to enter text. -# [3.5.3] +## 3.5.3 -- Change link button behavior. +* Change link button behavior. -# [3.5.2] +## 3.5.2 -- Bug fix for embed. +* Bug fix for embed. -# [3.5.1] +## 3.5.1 -- Bug fix for platform util. +* Bug fix for platform util. -# [3.5.0] +## 3.5.0 -- Removed redundant classes. +* Removed redundant classes. -# [3.4.4] +## 3.4.4 -- Add more translations. +* Add more translations. -# [3.4.3] +## 3.4.3 -- Preset link from attributes. +* Preset link from attributes. -# [3.4.2] +## 3.4.2 -- Fix launch link edit mode. +* Fix launch link edit mode. -# [3.4.1] +## 3.4.1 -- Placeholder effective in scrollable. +* Placeholder effective in scrollable. -# [3.4.0] +## 3.4.0 -- Option to save image in read-only mode. +* Option to save image in read-only mode. -# [3.3.1] +## 3.3.1 -- Pass any specified key in QuillEditor constructor to super. +* Pass any specified key in QuillEditor constructor to super. -# [3.3.0] +## 3.3.0 -- Fixed Style toggle issue. +* Fixed Style toggle issue. -# [3.2.1] +## 3.2.1 -- Added new translations. +* Added new translations. -# [3.2.0] +## 3.2.0 -- Support multiple links insertion on the go. +* Support multiple links insertion on the go. -# [3.1.1] +## 3.1.1 -- Add selection completed callback. +* Add selection completed callback. -# [3.1.0] +## 3.1.0 -- Fixed image ontap functionality. +* Fixed image ontap functionality. -# [3.0.4] +## 3.0.4 -- Add maxContentWidth constraint to editor. +* Add maxContentWidth constraint to editor. -# [3.0.3] +## 3.0.3 -- Do not show caret on screen when the editor is not focused. +* Do not show caret on screen when the editor is not focused. -# [3.0.2] +## 3.0.2 -- Fix launch link for read-only mode. +* Fix launch link for read-only mode. -# [3.0.1] +## 3.0.1 -- Handle null value of Attribute.link. +* Handle null value of Attribute.link. -# [3.0.0] +## 3.0.0 -- Launch link improvements. -- Removed QuillSimpleViewer. +* Launch link improvements. +* Removed QuillSimpleViewer. -# [2.5.2] +## 2.5.2 -- Skip image when pasting. +* Skip image when pasting. -# [2.5.1] +## 2.5.1 -- Bug fix for Desktop `Shift` + `Click` support. +* Bug fix for Desktop `Shift` + `Click` support. -# [2.5.0] +## 2.5.0 -- Update checkbox list. +* Update checkbox list. -# [2.4.1] +## 2.4.1 -- Desktop selection improvements. +* Desktop selection improvements. -# [2.4.0] +## 2.4.0 -- Improve inline code style. +* Improve inline code style. -# [2.3.3] +## 2.3.3 -- Improves selection rects to have consistent height regardless of individual segment text styles. +* Improves selection rects to have consistent height regardless of individual segment text styles. -# [2.3.2] +## 2.3.2 -- Allow disabling floating cursor. +* Allow disabling floating cursor. -# [2.3.1] +## 2.3.1 -- Preserve last newline character on delete. +* Preserve last newline character on delete. -# [2.3.0] +## 2.3.0 -- Massive changes to support flutter 2.8. +* Massive changes to support flutter 2.8. -# [2.2.2] +## 2.2.2 -- iOS - floating cursor. +* iOS - floating cursor. -# [2.2.1] +## 2.2.1 -- Bug fix for imports supporting flutter 2.8. +* Bug fix for imports supporting flutter 2.8. -# [2.2.0] +## 2.2.0 -- Support flutter 2.8. +* Support flutter 2.8. -# [2.1.1] +## 2.1.1 -- Add methods of clearing editor and moving cursor. +* Add methods of clearing editor and moving cursor. -# [2.1.0] +## 2.1.0 -- Add delete handler. +* Add delete handler. -# [2.0.23] +## 2.0.23 -- Support custom replaceText handler. +* Support custom replaceText handler. -# [2.0.22] +## 2.0.22 -- Fix attribute compare and fix font size parsing. +* Fix attribute compare and fix font size parsing. -# [2.0.21] +## 2.0.21 -- Handle click on embed object. +* Handle click on embed object. -# [2.0.20] +## 2.0.20 -- Improved UX/UI of Image widget. +* Improved UX/UI of Image widget. -# [2.0.19] +## 2.0.19 -- When uploading a video, applying indicator. +* When uploading a video, applying indicator. -# [2.0.18] +## 2.0.18 -- Make toolbar dividers optional. +* Make toolbar dividers optional. -# [2.0.17] +## 2.0.17 -- Allow alignment of the toolbar icons to match WrapAlignment. +* Allow alignment of the toolbar icons to match WrapAlignment. -# [2.0.16] +## 2.0.16 -- Add hide / show alignment buttons. +* Add hide / show alignment buttons. -# [2.0.15] +## 2.0.15 -- Implement change cursor to SystemMouseCursors.click when hovering a link styled text. +* Implement change cursor to SystemMouseCursors.click when hovering a link styled text. -# [2.0.14] +## 2.0.14 -- Enable customize the checkbox widget using DefaultListBlockStyle style. +* Enable customize the checkbox widget using DefaultListBlockStyle style. -# [2.0.13] +## 2.0.13 -- Improve the scrolling performance by reducing the repaint areas. +* Improve the scrolling performance by reducing the repaint areas. -# [2.0.12] +## 2.0.12 -- Fix the selection effect can't be seen as the textLine with background color. +* Fix the selection effect can't be seen as the textLine with background color. -# [2.0.11] +## 2.0.11 -- Fix visibility of text selection handlers on scroll. +* Fix visibility of text selection handlers on scroll. -# [2.0.10] +## 2.0.10 -- cursorConnt.color notify the text_line to repaint if it was disposed. +* cursorConnt.color notify the text_line to repaint if it was disposed. -# [2.0.9] +## 2.0.9 -- Improve UX when trying to add a link. +* Improve UX when trying to add a link. -# [2.0.8] +## 2.0.8 -- Adding translations to the toolbar. +* Adding translations to the toolbar. -# [2.0.7] +## 2.0.7 -- Added theming options for toolbar icons and LinkDialog. +* Added theming options for toolbar icons and LinkDialog. -# [2.0.6] +## 2.0.6 -- Avoid runtime error when placed inside TabBarView. +* Avoid runtime error when placed inside TabBarView. -# [2.0.5] +## 2.0.5 -- Support inline code formatting. +* Support inline code formatting. -# [2.0.4] +## 2.0.4 -- Enable history shortcuts for desktop. +* Enable history shortcuts for desktop. -# [2.0.3] +## 2.0.3 -- Fix cursor when line contains image. +* Fix cursor when line contains image. -# [2.0.2] +## 2.0.2 -- Address KeyboardListener class name conflict. +* Address KeyboardListener class name conflict. -# [2.0.1] +## 2.0.1 -- Upgrade flutter_colorpicker to 0.5.0. +* Upgrade flutter_colorpicker to 0.5.0. -# [2.0.0] +## 2.0.0 -- Text Alignment functions + Block Format standards. +* Text Alignment functions + Block Format standards. -# [1.9.6] +## 1.9.6 -- Support putting QuillEditor inside a Scrollable view. +* Support putting QuillEditor inside a Scrollable view. -# [1.9.5] +## 1.9.5 -- Skip image when pasting. +* Skip image when pasting. -# [1.9.4] +## 1.9.4 -- Bug fix for cursor position when tapping at the end of line with image(s). +* Bug fix for cursor position when tapping at the end of line with image(s). -# [1.9.3] +## 1.9.3 -- Bug fix when line only contains one image. +* Bug fix when line only contains one image. -# [1.9.2] +## 1.9.2 -- Support for building custom inline styles. +* Support for building custom inline styles. -# [1.9.1] +## 1.9.1 -- Cursor jumps to the most appropriate offset to display selection. +* Cursor jumps to the most appropriate offset to display selection. -# [1.9.0] +## 1.9.0 -- Support inline image. +* Support inline image. -# [1.8.3] +## 1.8.3 -- Updated quill_delta. +* Updated quill_delta. -# [1.8.2] +## 1.8.2 -- Support mobile image alignment. +* Support mobile image alignment. -# [1.8.1] +## 1.8.1 -- Support mobile custom size image. +* Support mobile custom size image. -# [1.8.0] +## 1.8.0 -- Support entering link for image/video. +* Support entering link for image/video. -# [1.7.3] +## 1.7.3 -- Bumps photo_view version. +* Bumps photo_view version. -# [1.7.2] +## 1.7.2 -- Fix static analysis error. +* Fix static analysis error. -# [1.7.1] +## 1.7.1 -- Support Youtube video. +* Support Youtube video. -# [1.7.0] +## 1.7.0 -- Support video. +* Support video. -# [1.6.4] +## 1.6.4 -- Bug fix for clear format button. +* Bug fix for clear format button. -# [1.6.3] +## 1.6.3 -- Fixed dragging right handle scrolling issue. +* Fixed dragging right handle scrolling issue. -# [1.6.2] +## 1.6.2 -- Fixed the position of the selection status drag handle. +* Fixed the position of the selection status drag handle. -# [1.6.1] +## 1.6.1 -- Upgrade image_picker and flutter_colorpicker. +* Upgrade image_picker and flutter_colorpicker. -# [1.6.0] +## 1.6.0 -- Support Multi Row Toolbar. +* Support Multi Row Toolbar. -# [1.5.0] +## 1.5.0 -- Remove file_picker dependency. +* Remove file_picker dependency. -# [1.4.1] +## 1.4.1 -- Remove filesystem_picker dependency. +* Remove filesystem_picker dependency. -# [1.4.0] +## 1.4.0 -- Remove path_provider dependency. +* Remove path_provider dependency. -# [1.3.4] +## 1.3.4 -- Add option to paintCursorAboveText. +* Add option to paintCursorAboveText. -# [1.3.3] +## 1.3.3 -- Upgrade file_picker version. +* Upgrade file_picker version. -# [1.3.2] +## 1.3.2 -- Fix copy/paste bug. +* Fix copy/paste bug. -# [1.3.1] +## 1.3.1 -- New logo. +* New logo. -# [1.3.0] +## 1.3.0 -- Support flutter 2.2.0. +* Support flutter 2.2.0. -# [1.2.2] +## 1.2.2 -- Checkbox supports tapping. +* Checkbox supports tapping. -# [1.2.1] +## 1.2.1 -- Indented position not holding while editing. +* Indented position not holding while editing. -# [1.2.0] +## 1.2.0 -- Fix image button cancel causes crash. +* Fix image button cancel causes crash. -# [1.1.8] +## 1.1.8 -- Fix height of empty line bug. +* Fix height of empty line bug. -# [1.1.7] +## 1.1.7 -- Fix text selection in read-only mode. +* Fix text selection in read-only mode. -# [1.1.6] +## 1.1.6 -- Remove universal_html dependency. +* Remove universal_html dependency. -# [1.1.5] +## 1.1.5 -- Enable "Select", "Select All" and "Copy" in read-only mode. +* Enable "Select", "Select All" and "Copy" in read-only mode. -# [1.1.4] +## 1.1.4 -- Fix text selection issue. +* Fix text selection issue. -# [1.1.3] +## 1.1.3 -- Update example folder. +* Update example folder. -# [1.1.2] +## 1.1.2 -- Add pedantic. +* Add pedantic. -# [1.1.1] +## 1.1.1 -- Base64 image support. +* Base64 image support. -# [1.1.0] +## 1.1.0 -- Support null safety. +* Support null safety. -# [1.0.9] +## 1.0.9 -- Web support for raw editor and keyboard listener. +* Web support for raw editor and keyboard listener. -# [1.0.8] +## 1.0.8 -- Support token attribute. +* Support token attribute. -# [1.0.7] +## 1.0.7 -- Fix crash on web (dart:io). +* Fix crash on web (dart:io). -# [1.0.6] +## 1.0.6 -- Add desktop support - WINDOWS, MACOS and LINUX. +* Add desktop support WINDOWS, MACOS and LINUX. -# [1.0.5] +## 1.0.5 -- Bug fix: Can not insert newline when Bold is toggled ON. +* Bug fix: Can not insert newline when Bold is toggled ON. -# [1.0.4] +## 1.0.4 -- Upgrade photo_view to ^0.11.0. +* Upgrade photo_view to ^0.11.0. -# [1.0.3] +## 1.0.3 -- Fix issue that text is not displayed while typing [WEB]. +* Fix issue that text is not displayed while typing WEB. -# [1.0.2] +## 1.0.2 -- Update toolbar in sample home page. +* Update toolbar in sample home page. -# [1.0.1] +## 1.0.1 -- Fix static analysis errors. +* Fix static analysis errors. -# [1.0.0] +## 1.0.0 -- Support flutter 2.0. +* Support flutter 2.0. -# [1.0.0-dev.2] +## 1.0.0-dev.2 -- Improve link handling for tel, mailto and etc. +* Improve link handling for tel, mailto and etc. -# [1.0.0-dev.1] +## 1.0.0-dev.1 -- Upgrade prerelease SDK & Bump for master. +* Upgrade prerelease SDK & Bump for master. -# [0.3.5] +## 0.3.5 -- Fix for cursor focus issues when keyboard is on. +* Fix for cursor focus issues when keyboard is on. -# [0.3.4] +## 0.3.4 -- Improve link handling for tel, mailto and etc. +* Improve link handling for tel, mailto and etc. -# [0.3.3] +## 0.3.3 -- More fix on cursor focus issue when keyboard is on. +* More fix on cursor focus issue when keyboard is on. -# [0.3.2] +## 0.3.2 -- Fix cursor focus issue when keyboard is on. +* Fix cursor focus issue when keyboard is on. -# [0.3.1] +## 0.3.1 -- cursor focus when keyboard is on. +* cursor focus when keyboard is on. -# [0.3.0] +## 0.3.0 -- Line Height calculated based on font size. +* Line Height calculated based on font size. -# [0.2.12] +## 0.2.12 -- Support placeholder. +* Support placeholder. -# [0.2.11] +## 0.2.11 -- Fix static analysis error. +* Fix static analysis error. -# [0.2.10] +## 0.2.10 -- Update TextInputConfiguration autocorrect to true in stable branch. +* Update TextInputConfiguration autocorrect to true in stable branch. -# [0.2.9] +## 0.2.9 -- Update TextInputConfiguration autocorrect to true. +* Update TextInputConfiguration autocorrect to true. -# [0.2.8] +## 0.2.8 -- Support display local image besides network image in stable branch. +* Support display local image besides network image in stable branch. -# [0.2.7] +## 0.2.7 -- Support display local image besides network image. +* Support display local image besides network image. -# [0.2.6] +## 0.2.6 -- Fix cursor after pasting. +* Fix cursor after pasting. -# [0.2.5] +## 0.2.5 -- Toggle text/background color button in toolbar. +* Toggle text/background color button in toolbar. -# [0.2.4] +## 0.2.4 -- Support the use of custom icon size in toolbar. +* Support the use of custom icon size in toolbar. -# [0.2.3] +## 0.2.3 -- Support custom styles and image on local device storage without uploading. +* Support custom styles and image on local device storage without uploading. -# [0.2.2] +## 0.2.2 -- Update git repo. +* Update git repo. -# [0.2.1] +## 0.2.1 -- Fix static analysis error. +* Fix static analysis error. -# [0.2.0] +## 0.2.0 -- Add checked/unchecked list button in toolbar. +* Add checked/unchecked list button in toolbar. -# [0.1.8] +## 0.1.8 -- Support font and size attributes. +* Support font and size attributes. -# [0.1.7] +## 0.1.7 -- Support checked/unchecked list. +* Support checked/unchecked list. -# [0.1.6] +## 0.1.6 -- Fix getExtentEndpointForSelection. +* Fix getExtentEndpointForSelection. -# [0.1.5] +## 0.1.5 -- Support text alignment. +* Support text alignment. -# [0.1.4] +## 0.1.4 -- Handle url with trailing spaces. +* Handle url with trailing spaces. -# [0.1.3] +## 0.1.3 -- Handle cursor position change when undo/redo. +* Handle cursor position change when undo/redo. -# [0.1.2] +## 0.1.2 -- Handle more text colors. +* Handle more text colors. -# [0.1.1] +## 0.1.1 -- Fix cursor issue when undo. +* Fix cursor issue when undo. -# [0.1.0] +## 0.1.0 -- Fix insert image. +* Fix insert image. -# [0.0.9] +## 0.0.9 -- Handle rgba color. +* Handle rgba color. -# [0.0.8] +## 0.0.8 -- Fix launching url. +* Fix launching url. -# [0.0.7] +## 0.0.7 -- Handle multiple image inserts. +* Handle multiple image inserts. -# [0.0.6] +## 0.0.6 -- More toolbar functionality. +* More toolbar functionality. -# [0.0.5] +## 0.0.5 -- Update example. +* Update example. -# [0.0.4] +## 0.0.4 -- Update example. +* Update example. -# [0.0.3] +## 0.0.3 -- Update home page meta data. +* Update home page meta data. -# [0.0.2] +## 0.0.2 -- Support image upload and launch url in read-only mode. +* Support image upload and launch url in read-only mode. -# [0.0.1] +## 0.0.1 -- Rich text editor based on Quill Delta. +* Rich text editor based on Quill Delta. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..85d81a00 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,64 @@ +# Contributing + +The contributions are more than welcome!
+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 \ No newline at end of file diff --git a/FETCH_HEAD b/FETCH_HEAD deleted file mode 100644 index e69de29b..00000000 diff --git a/LICENSE b/LICENSE index 498b3e0a..aaec6d89 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2020 Xin Yao +Copyright (c) 2023 Xin Yao Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 916a1642..3db5e322 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ [![Watch on GitHub][github-forks-badge]][github-forks-link] [license-badge]: https://img.shields.io/github/license/singerdmx/flutter-quill.svg?style=for-the-badge -[license-link]: https://github.com/singerdmx/flutter-quill/blob/master/LICENSE +[license-link]: ./LICENSE [prs-badge]: https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=for-the-badge [prs-link]: https://github.com/singerdmx/flutter-quill/issues [github-watch-badge]: https://img.shields.io/github/watchers/singerdmx/flutter-quill.svg?style=for-the-badge&logo=github&logoColor=ffffff @@ -26,51 +26,51 @@ FlutterQuill is a rich text editor and a [Quill] component for [Flutter]. -This library is a WYSIWYG editor built for the modern Android, iOS, web and desktop platforms. Check out our [Youtube Playlist] or [Code Introduction] to take a detailed walkthrough of the code base. You can join our [Slack Group] for discussion. +This library is a WYSIWYG (What You See Is What You Get) editor built +for the modern Android, iOS, +web and desktop platforms. +Check out our [Youtube Playlist] or [Code Introduction](./doc/code_introduction.md) +to take a detailed walkthrough of the code base. +You can join our [Slack Group] for discussion. -Pub: [FlutterQuill] +Pub: [FlutterQuill]
+If you are viewing this page from pub.dev page, then you +might have some issues with opening some links, please open +it in GitHub repo instead. ## Table of contents - [Flutter Quill](#flutter-quill) - [Table of contents](#table-of-contents) - - [Demo](#demo) + - [Screenshots](#screenshots) - [Installation](#installation) - [Usage](#usage) - [Migration](#migration) - [Input / Output](#input--output) + - [Links](#links) - [Configurations](#configurations) - - [Using Custom App Widget](#using-custom-app-widget) - - [Font Size](#font-size) + - [Links](#links-1) - [Font Family](#font-family) - - [Custom Buttons](#custom-buttons) - [Embed Blocks](#embed-blocks) - [Using the embed blocks from `flutter_quill_extensions`](#using-the-embed-blocks-from-flutter_quill_extensions) - - [Custom Size Image for Mobile](#custom-size-image-for-mobile) - - [Custom Size Image for other platforms (excluding web)](#custom-size-image-for-other-platforms-excluding-web) - - [Custom Embed Blocks](#custom-embed-blocks) - - [Custom Toolbar](#custom-toolbar) - - [Translation](#translation) - - [](#) - - [Contributing to translations](#contributing-to-translations) + - [Links](#links-2) - [Conversion to HTML](#conversion-to-html) + - [Translation](#translation) - [Testing](#testing) - - [License](#license) - [Contributors](#contributors) - - [Sponsors](#sponsors) -## Demo +## Screenshots -

- 1 - 1 -

+
+Tap to show/hide screenshots -

- 1 - 1 -

+
---- +Screenshot 1 +Screenshot 2 +Screenshot 3 +Screenshot 4 + +
## Installation @@ -87,24 +87,37 @@ dependencies: git: https://github.com/singerdmx/flutter-quill.git ``` -> **Important note** + > -> Currently, we're in the process of refactoring the library's configurations. We're actively working on this, and while we don't have a development version available at the moment, your feedback is essential to us. +> Note: At this time, we are making too many changes to the library, and you might see a new version almost every day > -> Using the latest version and reporting any issues you encounter on GitHub will greatly contribute to the improvement of the library. Your input 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! +> Using the latest version and reporting any issues you encounter on GitHub will greatly contribute to the improvement of the library. +> Your input 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! > -> also [flutter_quill_extensions](https://pub.dev/packages/flutter_quill_extensions) will not work with the latest versions, please use [fresh_quill_extensions](https://pub.dev/packages/fresh_quill_extensions) as temporary alternative +> If the latest version of [FlutterQuill Extensions] is pre-release, then please use it to work with the latest stable version of [FlutterQuill] > +Compatible versions: + +| flutter_quill | flutter_quill_extensions | flutter_quill_test | +|-------------------------|--------------------------|-------------------------| +| 8.5.x | 0.6.x | 0.0.5 | +| 8.5.1 | 0.6.7 | 0.0.5 | +| 8.5.0 | 0.6.7 | 0.0.5 | + +These versions are tested and well-supported, you shouldn't get a build failure + ## Usage -See the `example` directory for a minimal example of how to use FlutterQuill. You typically just need to instantiate a controller: +First, you need to instantiate a controller ```dart QuillController _controller = QuillController.basic(); ``` -and then embed the toolbar and the editor, within your app. For example: +And then use the `QuillEditor`, `QuillToolbar` widgets, +connect the `QuillController` to them +using `QuillProvider` inherited widget ```dart QuillProvider( @@ -129,147 +142,75 @@ QuillProvider( ) ``` -And depending on your use case, you might want to dispose the `_controller` in dispose mehtod +And depending on your use case, you might want to dispose the `_controller` in dispose method + +in most cases, it's better to. Check out [Sample Page] for more advanced usage. ## Migration -We have recently add [migration guide](/doc/migration.md) for migration from different versions +Starting from version `8.0.0` +We have added [Migration Guide](/doc/migration.md) for migration from different versions ## Input / Output -This library uses [Quill] as an internal data format. +This library uses [Quill Delta](https://quilljs.com/docs/delta/) +to represent the document content. +The Delta format is a compact and versatile way to describe document changes. +It consists of a series of operations, each representing an insertion, deletion, +or formatting change within the document. + +Don’t be confused by its name Delta—Deltas represents both documents and changes to documents. +If you think of Deltas as the instructions from going from one document to another, +the way Deltas represent a document is by expressing the instructions starting from an empty document. * Use `_controller.document.toDelta()` to extract the deltas. * Use `_controller.document.toPlainText()` to extract plain text. -FlutterQuill provides some JSON serialization support, so that you can save and open documents. To save a document as JSON, do something like the following: +FlutterQuill provides some JSON serialization support, so that you can save and open documents. +To save a document as JSON, do something like the following: ```dart -var json = jsonEncode(_controller.document.toDelta().toJson()); +final json = jsonEncode(_controller.document.toDelta().toJson()); ``` You can then write this to storage. -To open a FlutterQuill editor with an existing JSON representation that you've previously stored, you can do something like this: +To open a FlutterQuill editor with an existing JSON representation that you've previously stored, +you can do something like this: ```dart -var myJSON = jsonDecode(r'{"insert":"hello\n"}'); -_controller = QuillController( - document: Document.fromJson(myJSON), - selection: TextSelection.collapsed(offset: 0), - ); -``` - -## Configurations - -The `QuillToolbar` class lets you customize which formatting options are available. -[Sample Page] provides sample code for advanced usage and configuration. - -For **web development**, use `flutter config --enable-web` for flutter or use [ReactQuill] for React. - -It is required to provide `EmbedBuilder`, e.g. [defaultEmbedBuildersWeb](https://github.com/singerdmx/flutter-quill/blob/master/example/lib/universal_ui/universal_ui.dart#L99). -Also it is required to provide `webImagePickImpl`, e.g. [Sample Page](https://github.com/singerdmx/flutter-quill/blob/master/example/lib/pages/home_page.dart#L317). - -For **desktop platforms** It is required to provide `filePickImpl` for toolbar image button, e.g. [Sample Page](https://github.com/singerdmx/flutter-quill/blob/master/example/lib/pages/home_page.dart#L297). - - -### Using Custom App Widget +final json = jsonDecode(r'{"insert":"hello\n"}'); -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](https://pub.dev/packages/fluent_ui) -which will also need - -```dart -localizationsDelegates: const [ - // Required localizations delegates ... - FluentLocalizations.delegate, - AppLocalizations.delegate, -], +_controller.document = Document.fromJson(json); ``` -in addition to the required delegates by this library - -### Font Size - -Within the editor toolbar, a drop-down with font-sizing capabilities is available. This can be enabled or disabled with `showFontSize`. +### Links -When enabled, the default font-size values can be modified via _optional_ `fontSizeValues`. `fontSizeValues` accepts a `Map` consisting of a `String` title for the font size and a `String` value for the font size. Example: +- [Quill Delta](https://quilljs.com/docs/delta/) +- [Quill Delta Formats](https://quilljs.com/docs/formats) +- [Why Quill](https://quilljs.com/guides/why-quill/) +- [Quill JS Configurations](https://quilljs.com/docs/configuration/) +- [Quill JS Interactive Playground](https://quilljs.com/playground/) +- [Quill JS GitHub repo](https://github.com/quilljs/quill) -```dart -fontSizeValues: const {'Small': '8', 'Medium': '24.5', 'Large': '46'} -``` +## Configurations -Font size can be cleared with a value of `0`, for example: +The `QuillToolbar` and `QuillEditor` widgets lets you customize a lot of things +[Sample Page] provides sample code for advanced usage and configuration. -```dart -fontSizeValues: const {'Small': '8', 'Medium': '24.5', 'Large': '46', 'Clear': '0'} -``` +### Links +- [Using Custom App Widget](./doc/configurations/using_custom_app_widget.md) +- [Localizations Setup](./doc/configurations/localizations_setup.md) +- [Font Size](./doc/configurations/font_size.md) +- [Font Family](#font-family) +- [Custom Toolbar buttons](./doc/configurations/custom_buttons.md) ### Font Family -To use your own fonts, update your [assets folder](https://github.com/singerdmx/flutter-quill/tree/master/example/assets/fonts) and pass in `fontFamilyValues`. More details at [this change](https://github.com/singerdmx/flutter-quill/commit/71d06f6b7be1b7b6dba2ea48e09fed0d7ff8bbaa), [this article](https://stackoverflow.com/questions/55075834/fontfamily-property-not-working-properly-in-flutter) and [this](https://www.flutterbeads.com/change-font-family-flutter/). - -### Custom Buttons - -You may add custom buttons to the _end_ of the toolbar, via the `customButtons` option, which is a `List` of `QuillCustomButton`. - -To add an Icon, we should use a new QuillCustomButton class - -```dart - QuillCustomButton( - iconData: Icons.ac_unit, - onTap: () { - debugPrint('snowflake'); - } - ), -``` - -Each `QuillCustomButton` is used as part of the `customButtons` option as follows: - -```dart -QuillToolbar( - configurations: QuillToolbarConfigurations( - customButtons: [ - QuillCustomButton( - iconData: Icons.ac_unit, - onTap: () { - debugPrint('snowflake1'); - }, - ), - QuillCustomButton( - iconData: Icons.ac_unit, - onTap: () { - debugPrint('snowflake2'); - }, - ), - QuillCustomButton( - iconData: Icons.ac_unit, - onTap: () { - debugPrint('snowflake3'); - }, - ), - ], - ), -), -``` +To use your own fonts, update your [assets folder](./example/assets/fonts) and pass in `fontFamilyValues`. +More details on [this commit](https://github.com/singerdmx/flutter-quill/commit/71d06f6b7be1b7b6dba2ea48e09fed0d7ff8bbaa), +[this article](https://stackoverflow.com/questions/55075834/fontfamily-property-not-working-properly-in-flutter) and [this](https://www.flutterbeads.com/change-font-family-flutter/). ## Embed Blocks @@ -279,454 +220,45 @@ Provide a list of embed ### Using the embed blocks from `flutter_quill_extensions` -```dart -QuillToolbar( - configurations: QuillToolbarConfigurations( - embedButtons: FlutterQuillEmbeds.toolbarButtons( - imageButtonOptions: QuillToolbarImageButtonOptions( - onImagePickCallback: (file) async { - return file.path; - }, - ), - ), - ), -), -``` +To see how to use the extension package, please take a look at the [README](./flutter_quill_extensions/README.md) of [FlutterQuill Extensions] -```dart -Expanded( - child: QuillEditor.basic( - configurations: QuillEditorConfigurations( - readOnly: true, - embedBuilders: FlutterQuillEmbeds.editorBuilders( - imageEmbedConfigurations: - const QuillEditorImageEmbedConfigurations( - forceUseMobileOptionMenuForImageClick: true, - ), - ), - ), - ), -) -``` +### Links - - - -### Custom Size Image for Mobile - -Define `mobileWidth`, `mobileHeight`, `mobileMargin`, `mobileAlignment` as follows: - -```dart -{ - "insert": { - "image": "https://user-images.githubusercontent.com/122956/72955931-ccc07900-3d52-11ea-89b1-d468a6e2aa2b.png" - }, - "attributes":{ - "style":"mobileWidth: 50; mobileHeight: 50; mobileMargin: 10; mobileAlignment: topLeft" - } -} -``` - -### Custom Size Image for other platforms (excluding web) - -Define `width`, `height`, `margin`, `alignment` as follows: - -```dart -{ - "insert": { - "image": "https://user-images.githubusercontent.com/122956/72955931-ccc07900-3d52-11ea-89b1-d468a6e2aa2b.png" - }, - "attributes":{ - "style":"width: 50; height: 50; margin: 10; alignment: topLeft" - } -} -``` - -### 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 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 _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! - -

- 1 -

- -> 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) - - -### 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 - -### Translation - -The package offers translations for the quill toolbar and editor, it will follow the system locale unless you set your own locale with: - -```dart - QuillProvider( - configurations: QuillConfigurations( - controller: _controller, - sharedConfigurations: const QuillSharedConfigurations( - locale: Locale('fr'), - ), - ), - 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 file is located at [toolbar.i18n.dart](lib/src/translations/toolbar.i18n.dart). Feel free to contribute your own translations, just copy the English translations map and replace the values with your translations. Then open a pull request so everyone can benefit from your translations! +- [Custom Embed Blocks](./doc/custom_embed_blocks.md) +- [Custom Toolbar](./doc/custom_toolbar.md) ## Conversion to HTML Having your document stored in Quill Delta format is sometimes not enough. Often you'll need to convert -it to other formats such as HTML in order to publish it, or send an email. One option is to use +it to other formats such as HTML to publish it, or send an email. + +You have two options: + +1. Using [quill_html_converter](./packages/quill_html_converter/) to convert to/from HTML, the package can convert the Quill delta to HTML well +(it uses [vsc_quill_delta_to_html](https://pub.dev/packages/vsc_quill_delta_to_html)) but the converting from HTML back to Quill delta is experimental +2. Another option is to use [vsc_quill_delta_to_html](https://pub.dev/packages/vsc_quill_delta_to_html) to convert your document -to HTML. This package has full support for all Quill operations - including images, videos, formulas, -tables, and mentions. Conversion can be performed in vanilla Dart (i.e., server-side or CLI) or in Flutter. +to HTML. + This package has full support for all Quill operations—including images, videos, formulas, +tables, and mentions. + Conversion can be performed in vanilla Dart (i.e., server-side or CLI) or in Flutter. It is a complete Dart part of the popular and mature [quill-delta-to-html](https://www.npmjs.com/package/quill-delta-to-html) Typescript/Javascript package. + this package doesn't convert the HTML back to Quill Delta as far as we know -## Testing - -To aid in testing applications using the editor an extension to the flutter `WidgetTester` is provided which includes methods to simplify interacting with the editor in test cases. - -Import the test utilities in your test file: - -```dart -import 'package:flutter_quill/flutter_quill_test.dart'; -``` +## Translation -and then enter text using `quillEnterText`: +The package offers translations for the quill toolbar and editor, it will follow the system locale unless you set your own locale. -```dart -await tester.quillEnterText(find.byType(QuillEditor), 'test\n'); -``` +Open this [page](./doc/translation.md) for more info -## License +## Testing -[MIT](LICENSE) +Please use [flutter_quill_test](https://pub.dev/packages/flutter_quill_test) for testing ## Contributors -Special thanks for everyone that have contributed to this project... +Special thanks for everyone that has contributed to this project... @@ -736,15 +268,22 @@ Special thanks for everyone that have contributed to this project... Made with [contrib.rocks](https://contrib.rocks). +We welcome contributions! + +Please follow these guidelines when contributing to the project. See [CONTRIBUTING.md](./CONTRIBUTING.md) for more details.
+ +We must mention that the [CONTRIBUTING.md](./CONTRIBUTING.md) have a development notes, so if you're planning on contributing to the repo, +please consider reading it. + +You can check the [Todo](./doc/todo.md) list if you want to + [Quill]: https://quilljs.com/docs/formats [Flutter]: https://github.com/flutter/flutter [FlutterQuill]: https://pub.dev/packages/flutter_quill +[FlutterQuill Extensions]: https://pub.dev/packages/flutter_quill_extensions [ReactQuill]: https://github.com/zenoamaro/react-quill [Youtube Playlist]: https://youtube.com/playlist?list=PLbhaS_83B97vONkOAWGJrSXWX58et9zZ2 [Slack Group]: https://join.slack.com/t/bulletjournal1024/shared_invite/zt-fys7t9hi-ITVU5PGDen1rNRyCjdcQ2g -[Sample Page]: https://github.com/singerdmx/flutter-quill/blob/master/example/lib/pages/home_page.dart -[Code Introduction]: https://github.com/singerdmx/flutter-quill/blob/master/CodeIntroduction.md - -
+[Sample Page]: ./example/lib/pages/home_page.dart +[FluentUI]: https://pub.dev/packages/fluent_ui -[中文文档](./doc_cn.md) diff --git a/analysis_options.yaml b/analysis_options.yaml index 7749c861..f1a38172 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -1,4 +1,4 @@ -include: package:pedantic/analysis_options.yaml +include: package:flutter_lints/flutter.yaml analyzer: errors: @@ -6,32 +6,31 @@ analyzer: unsafe_html: ignore linter: rules: - - always_declare_return_types - - always_put_required_named_parameters_first - - annotate_overrides - - avoid_empty_else - - avoid_escaping_inner_quotes - - avoid_print - - avoid_redundant_argument_values - - avoid_types_on_closure_parameters - - avoid_void_async - - cascade_invocations - - directives_ordering - - lines_longer_than_80_chars - - omit_local_variable_types - - prefer_const_constructors - - prefer_const_constructors_in_immutables - - prefer_const_declarations - - prefer_final_fields - - prefer_final_in_for_each - - prefer_final_locals - - prefer_initializing_formals - - prefer_int_literals - - prefer_interpolation_to_compose_strings - - prefer_relative_imports - - prefer_single_quotes - - sort_constructors_first - - sort_unnamed_constructors_first - - unnecessary_lambdas - - unnecessary_parenthesis - - unnecessary_string_interpolations + 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: true + avoid_redundant_argument_values: true + 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 diff --git a/CodeIntroduction.md b/doc/code_introduction.md similarity index 100% rename from CodeIntroduction.md rename to doc/code_introduction.md diff --git a/doc/configurations/custom_buttons.md b/doc/configurations/custom_buttons.md new file mode 100644 index 00000000..24f9e1ca --- /dev/null +++ b/doc/configurations/custom_buttons.md @@ -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'); + }, + ), + ], + ), +), +``` \ No newline at end of file diff --git a/doc/configurations/font_size.md b/doc/configurations/font_size.md new file mode 100644 index 00000000..c73ddcb8 --- /dev/null +++ b/doc/configurations/font_size.md @@ -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` 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'} +``` \ No newline at end of file diff --git a/doc/configurations/localizations_setup.md b/doc/configurations/localizations_setup.md new file mode 100644 index 00000000..1d373a27 --- /dev/null +++ b/doc/configurations/localizations_setup.md @@ -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 \ No newline at end of file diff --git a/doc/configurations/using_custom_app_widget.md b/doc/configurations/using_custom_app_widget.md new file mode 100644 index 00000000..8a69f9d0 --- /dev/null +++ b/doc/configurations/using_custom_app_widget.md @@ -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 \ No newline at end of file diff --git a/doc/custom_embed_blocks.md b/doc/custom_embed_blocks.md new file mode 100644 index 00000000..82a1e44c --- /dev/null +++ b/doc/custom_embed_blocks.md @@ -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 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 _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! + +

+ 1 +

+ +> 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) \ No newline at end of file diff --git a/doc/custom_toolbar.md b/doc/custom_toolbar.md new file mode 100644 index 00000000..06c8bb72 --- /dev/null +++ b/doc/custom_toolbar.md @@ -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 \ No newline at end of file diff --git a/doc/development_notes.md b/doc/development_notes.md new file mode 100644 index 00000000..603e40b2 --- /dev/null +++ b/doc/development_notes.md @@ -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 \ No newline at end of file diff --git a/doc/migration.md b/doc/migration.md index 4de759f7..7a65b3ef 100644 --- a/doc/migration.md +++ b/doc/migration.md @@ -79,10 +79,14 @@ All the options have parent `QuillToolbarBaseButtonOptions` which have common th final IconData? iconData; /// To change the icon size pass a different value, by default will be - /// [kDefaultIconSize] + /// [kDefaultIconSize]. /// This will be used for all the buttons but you can override this final double globalIconSize; + /// The factor of how much larger the button is in relation to the icon, + /// by default it will be [kIconButtonFactor]. + final double globalIconButtonFactor; + /// To do extra logic after pressing the button final VoidCallback? afterButtonPressed; diff --git a/doc_cn.md b/doc/readme/cn.md similarity index 99% rename from doc_cn.md rename to doc/readme/cn.md index 680b4d72..098110dd 100644 --- a/doc_cn.md +++ b/doc/readme/cn.md @@ -24,7 +24,7 @@ --- -> This documentation is outdated. Please check the English version. +> This documentation is outdated. Please check the [English version](../../README.md). `FlutterQuill` 是一个富文本编辑器,也是 [Quill](https://quilljs.com/docs/formats) 在 [Flutter](https://github.com/flutter/flutter) 的版本 diff --git a/doc/todo.md b/doc/todo.md new file mode 100644 index 00000000..0dd2f43f --- /dev/null +++ b/doc/todo.md @@ -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. \ No newline at end of file diff --git a/doc/translation.md b/doc/translation.md new file mode 100644 index 00000000..a33d3213 --- /dev/null +++ b/doc/translation.md @@ -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! \ No newline at end of file diff --git a/example/.gitignore b/example/.gitignore index 15e3a7c7..24476c5d 100644 --- a/example/.gitignore +++ b/example/.gitignore @@ -8,6 +8,7 @@ .buildlog/ .history .svn/ +migrate_working_dir/ # IntelliJ related *.iml @@ -31,12 +32,13 @@ .pub/ /build/ -# Web related -lib/generated_plugin_registrant.dart - # Symbolication related app.*.symbols # Obfuscation related app.*.map.json -pubspec.lock \ No newline at end of file + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release diff --git a/example/.metadata b/example/.metadata index 24544cb7..a778330b 100644 --- a/example/.metadata +++ b/example/.metadata @@ -4,7 +4,42 @@ # This file should be version controlled and should not be manually edited. version: - revision: 84f3d28555368a70270e9ac8390a9441df95e752 - channel: stable + revision: "d211f42860350d914a5ad8102f9ec32764dc6d06" + channel: "stable" project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: d211f42860350d914a5ad8102f9ec32764dc6d06 + base_revision: d211f42860350d914a5ad8102f9ec32764dc6d06 + - platform: android + create_revision: d211f42860350d914a5ad8102f9ec32764dc6d06 + base_revision: d211f42860350d914a5ad8102f9ec32764dc6d06 + - platform: ios + create_revision: d211f42860350d914a5ad8102f9ec32764dc6d06 + base_revision: d211f42860350d914a5ad8102f9ec32764dc6d06 + - platform: linux + create_revision: d211f42860350d914a5ad8102f9ec32764dc6d06 + base_revision: d211f42860350d914a5ad8102f9ec32764dc6d06 + - platform: macos + create_revision: d211f42860350d914a5ad8102f9ec32764dc6d06 + base_revision: d211f42860350d914a5ad8102f9ec32764dc6d06 + - platform: web + create_revision: d211f42860350d914a5ad8102f9ec32764dc6d06 + base_revision: d211f42860350d914a5ad8102f9ec32764dc6d06 + - platform: windows + create_revision: d211f42860350d914a5ad8102f9ec32764dc6d06 + base_revision: d211f42860350d914a5ad8102f9ec32764dc6d06 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/example/README.md b/example/README.md index 5c0ad6d4..c727691c 100644 --- a/example/README.md +++ b/example/README.md @@ -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: +Screenshot 1 +Screenshot 2 +Screenshot 3 +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 +``` \ No newline at end of file diff --git a/example/analysis_options.yaml b/example/analysis_options.yaml new file mode 100644 index 00000000..735e0400 --- /dev/null +++ b/example/analysis_options.yaml @@ -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 \ No newline at end of file diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle index 4eef2899..d2bb9117 100644 --- a/example/android/app/build.gradle +++ b/example/android/app/build.gradle @@ -1,3 +1,9 @@ +plugins { + id "com.android.application" + id "kotlin-android" + id "dev.flutter.flutter-gradle-plugin" +} + def localProperties = new Properties() def localPropertiesFile = rootProject.file('local.properties') if (localPropertiesFile.exists()) { @@ -6,11 +12,6 @@ if (localPropertiesFile.exists()) { } } -def flutterRoot = localProperties.getProperty('flutter.sdk') -if (flutterRoot == null) { - throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") -} - def flutterVersionCode = localProperties.getProperty('flutter.versionCode') if (flutterVersionCode == null) { flutterVersionCode = '1' @@ -21,16 +22,14 @@ if (flutterVersionName == null) { flutterVersionName = '1.0' } -apply plugin: 'com.android.application' -apply plugin: 'kotlin-android' -apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" - android { - compileSdkVersion flutter.compileSdkVersion + namespace "com.example.example" + compileSdk flutter.compileSdkVersion + ndkVersion flutter.ndkVersion compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 } kotlinOptions { @@ -42,17 +41,15 @@ android { } defaultConfig { - applicationId "com.example.app" - minSdkVersion 21 + applicationId "com.example.example" + minSdkVersion 23 targetSdkVersion flutter.targetSdkVersion versionCode flutterVersionCode.toInteger() versionName flutterVersionName - // Multidex is not required for api level 21 } buildTypes { release { - // Signing with the debug keys for now, so `flutter run --release` works. signingConfig signingConfigs.debug } } @@ -62,6 +59,4 @@ flutter { source '../..' } -dependencies { - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" -} +dependencies {} diff --git a/example/android/app/src/debug/AndroidManifest.xml b/example/android/app/src/debug/AndroidManifest.xml index 9bc03bb2..399f6981 100644 --- a/example/android/app/src/debug/AndroidManifest.xml +++ b/example/android/app/src/debug/AndroidManifest.xml @@ -1,8 +1,7 @@ - - - diff --git a/example/android/app/src/main/AndroidManifest.xml b/example/android/app/src/main/AndroidManifest.xml index aeb699d6..e047f006 100644 --- a/example/android/app/src/main/AndroidManifest.xml +++ b/example/android/app/src/main/AndroidManifest.xml @@ -1,38 +1,54 @@ - - + + + + - + - - - - + + + + + + + android:icon="@mipmap/ic_launcher" + android:label="@string/app_name"> + android:name="io.flutter.embedding.android.NormalTheme" + android:resource="@style/NormalTheme" /> - - + + + + - diff --git a/example/android/build.gradle b/example/android/build.gradle index 85ed7b51..d73bed24 100644 --- a/example/android/build.gradle +++ b/example/android/build.gradle @@ -1,12 +1,14 @@ +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + buildscript { - ext.kotlin_version = '1.9.0' + ext.kotlin_version = '1.9.20' repositories { google() mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:7.4.0' + classpath 'com.android.tools.build:gradle:8.1.3' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } @@ -21,6 +23,47 @@ allprojects { rootProject.buildDir = '../build' subprojects { project.buildDir = "${rootProject.buildDir}/${project.name}" + + // For mode details visit https://gist.github.com/freshtechtips/93fefb39e48c40592bda3931e05fd35c + afterEvaluate { + // check if android block is available + + if (it.hasProperty('android')) { + + if (it.android.namespace == null) { + def manifest = new XmlSlurper().parse(file(it.android.sourceSets.main.manifest.srcFile)) + def packageName = manifest.@package.text() + println("Setting ${packageName} as android namespace in build.gradle from the AndroidManifest.xml") + android.namespace = packageName + } + + def javaVersion = JavaVersion.VERSION_17 + println("Changes will be applied for the following packages:") + android { + def androidApiVersion = 34 +// compileSdkVersion androidApiVersion + compileSdk androidApiVersion + defaultConfig { + targetSdkVersion androidApiVersion + } + compileOptions { + sourceCompatibility javaVersion + targetCompatibility javaVersion + } + tasks.withType(KotlinCompile).configureEach { + buildscript { + ext.kotlin_version = kotlin_version + } + kotlinOptions { + jvmTarget = javaVersion.toString() + } + } + String message = "For package ${android.namespace} by update compileSdkVersion, targetSdkVersion \n to $androidApiVersion and java version to ${javaVersion.toString()}" + println(message) + } + } + + } } subprojects { project.evaluationDependsOn(':app') diff --git a/example/android/gradle.properties b/example/android/gradle.properties index 94adc3a3..b9a9a246 100644 --- a/example/android/gradle.properties +++ b/example/android/gradle.properties @@ -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 diff --git a/example/android/gradle/wrapper/gradle-wrapper.properties b/example/android/gradle/wrapper/gradle-wrapper.properties index 6b665338..8bc9958a 100644 --- a/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/example/android/gradle/wrapper/gradle-wrapper.properties @@ -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 diff --git a/example/android/settings.gradle b/example/android/settings.gradle index 44e62bcf..55c4ca8b 100644 --- a/example/android/settings.gradle +++ b/example/android/settings.gradle @@ -1,11 +1,20 @@ -include ':app' +pluginManagement { + def flutterSdkPath = { + def properties = new Properties() + file("local.properties").withInputStream { properties.load(it) } + def flutterSdkPath = properties.getProperty("flutter.sdk") + assert flutterSdkPath != null, "flutter.sdk not set in local.properties" + return flutterSdkPath + } + settings.ext.flutterSdkPath = flutterSdkPath() -def localPropertiesFile = new File(rootProject.projectDir, "local.properties") -def properties = new Properties() + includeBuild("${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle") -assert localPropertiesFile.exists() -localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } + plugins { + id "dev.flutter.flutter-gradle-plugin" version "1.0.0" apply false + } +} -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" +include ":app" + +apply from: "${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle/app_plugin_loader.gradle" diff --git a/example/assets/images/screenshot_1.png b/example/assets/images/screenshot_1.png new file mode 100644 index 00000000..b42afad0 Binary files /dev/null and b/example/assets/images/screenshot_1.png differ diff --git a/example/assets/images/screenshot_2.png b/example/assets/images/screenshot_2.png new file mode 100644 index 00000000..c492b3c8 Binary files /dev/null and b/example/assets/images/screenshot_2.png differ diff --git a/example/assets/images/screenshot_3.png b/example/assets/images/screenshot_3.png new file mode 100644 index 00000000..3debe473 Binary files /dev/null and b/example/assets/images/screenshot_3.png differ diff --git a/example/assets/images/screenshot_4.png b/example/assets/images/screenshot_4.png new file mode 100644 index 00000000..cafcd86e Binary files /dev/null and b/example/assets/images/screenshot_4.png differ diff --git a/example/assets/sample_data.json b/example/assets/sample_data.json deleted file mode 100644 index 467bcadd..00000000 --- a/example/assets/sample_data.json +++ /dev/null @@ -1,540 +0,0 @@ -[ - { - "insert": { - "image": "iVBORw0KGgoAAAANSUhEUgAAAdIAAAHSCAYAAABYYEo2AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA3hpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMDY3IDc5LjE1Nzc0NywgMjAxNS8wMy8zMC0yMzo0MDo0MiAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDphZDc4NWQ1MS1mMWI2LTQzZDUtOWYxOC00MGRiZWZhNWZjYWIiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6NDRBMkRDNkEzNUI4MTFFQTk0QTlDRTRGNzJBQkE1N0UiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6NDRBMkRDNjkzNUI4MTFFQTk0QTlDRTRGNzJBQkE1N0UiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTUgKE1hY2ludG9zaCkiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDphZDc4NWQ1MS1mMWI2LTQzZDUtOWYxOC00MGRiZWZhNWZjYWIiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6YWQ3ODVkNTEtZjFiNi00M2Q1LTlmMTgtNDBkYmVmYTVmY2Fi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rIIIdDVCG4Uv77DDDq+pwhak+re1xX9fa5BGUn7r1q0/4GERbCSfUaBAgQIFqkIgySAc4iPFGtKiRYuV7vuJ2QS92sCjax2Qbr/99vN4v3DhQv8gg1YaKFCgQPkFUgCUlptooe3atZvmvv8mU3Gc2lIXvdahjANSn0tKsBFl0mx1jUCBAgUKVDXk24mlTLXk8X/44YdewenQocN4uTQy1cANQFrDJCIempN+PmjatKmPFps7d24ZbTVQoECBAlW9ZgqIErVLsfltttnmVYFotnrpAUhrCCHxoHluueWW86g1yvtp06bVmgcVKFCgQDWdB4umT5/uC9jTUtAB6WvJY22qTADSGkgtWrT4ori4eAYS0NSpU32ZquAnDRQoUKD8aKNSYgDLNm3azNh6660XWABNgmgA0hpGSoVxYPow7+XsDo2LAwUKFCg/hPKyYMECz3ebN2/+MHV2rRYqXl2rhIjaZlrAnHDooYc+BKC+99570bvvvuvBVJ3ObeAR+9SdIFCgQIECrR3ZOJTPPvssmjx5sldq9tprr4dVEKQ2aaC1Gkh5mCQAY05o2bLlDID19ddfTyfB86qoMklHyQbJgQIFChRozQiwVLGFl19+2Ss0TZs2ndG1a9d360JBkFoFpAJHB6RR586dHwZYX3zxxXTbKktoprLp2z6OgQIFChRozYhKRdJKx40b5y19Tpl5uF27dnXi/mudaReiqfFuu+32EFISZapUwF698SArJYU800CBAgVadyLt8IMPPvCKS7du3R6mXGsA0kK7GadhyufZu3fvGY0bN55BdSMkJNU5TRbw5oGHWqSBAgUKtPaEMgIfHT9+vC+E42jG/vvv/y51dgOQFhDJrACYpqShaMcdd3yI/aNGjfLf4R+1lY44zpp4AwUKFCjQ2vPfV1991VsAyZzYbbfd/D5rCQxAWsPJtpBC60T73HfffR+msgYRZETvQuzXg1W7n0CBAgUKtPZE0OYXX3wRTZw40fPfffbZ56HNNtusztx/rWrsLTAljwkqKSmZ0ahRoxnY7d98803fG89KT3ofSggGChQo0LpppMSjoLA0bNgQs+4M+DGuNrVMC0BaAIRmqfwkHNw8QKeRRt27d3/o22+/je69915f95Fms3wvXyl2/WDaDRQoUKDslKklJTxUfBQees8990Rff/111L59+4f32msvb/2rK+mF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" - }, - "attributes": { - "width": "230", - "style": "display: block; margin: auto; mobileWidth: 50; mobileHeight: 50; mobileMargin: 10; mobileAlignment: topLeft" - } - }, - { - "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" - }, - { - "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" - } -] \ No newline at end of file diff --git a/example/assets/sample_data_nomedia.json b/example/assets/sample_data_nomedia.json deleted file mode 100644 index e541d153..00000000 --- a/example/assets/sample_data_nomedia.json +++ /dev/null @@ -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" - } -] \ No newline at end of file diff --git a/example/ios/.gitignore b/example/ios/.gitignore index e96ef602..7a7f9873 100644 --- a/example/ios/.gitignore +++ b/example/ios/.gitignore @@ -1,3 +1,4 @@ +**/dgph *.mode1v3 *.mode2v3 *.moved-aside @@ -18,6 +19,7 @@ Flutter/App.framework Flutter/Flutter.framework Flutter/Flutter.podspec Flutter/Generated.xcconfig +Flutter/ephemeral/ Flutter/app.flx Flutter/app.zip Flutter/flutter_assets/ diff --git a/example/ios/Flutter/AppFrameworkInfo.plist b/example/ios/Flutter/AppFrameworkInfo.plist index 4f8d4d24..9625e105 100644 --- a/example/ios/Flutter/AppFrameworkInfo.plist +++ b/example/ios/Flutter/AppFrameworkInfo.plist @@ -3,7 +3,7 @@ CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) + en CFBundleExecutable App CFBundleIdentifier diff --git a/example/ios/Podfile b/example/ios/Podfile index 88359b22..fdcc671e 100644 --- a/example/ios/Podfile +++ b/example/ios/Podfile @@ -32,6 +32,9 @@ target 'Runner' do use_modular_headers! flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + end end post_install do |installer| diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj index 01a3496a..75c0e507 100644 --- a/example/ios/Runner.xcodeproj/project.pbxproj +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -9,14 +9,23 @@ /* Begin PBXBuildFile section */ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; - 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; }; - 97C146F31CF9000F007C117D /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 97C146F21CF9000F007C117D /* main.m */; }; + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; - D290BBC2BCE42906E260DD85 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DC245C6D0FF6BF1D0B733C5B /* Pods_Runner.framework */; }; + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; /* End PBXBuildFile section */ +/* Begin PBXContainerItemProxy section */ + 331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97C146E61CF9000F007C117D /* Project object */; + proxyType = 1; + remoteGlobalIDString = 97C146ED1CF9000F007C117D; + remoteInfo = Runner; + }; +/* End PBXContainerItemProxy section */ + /* Begin PBXCopyFilesBuildPhase section */ 9705A1C41CF9048500538489 /* Embed Frameworks */ = { isa = PBXCopyFilesBuildPhase; @@ -33,22 +42,19 @@ /* Begin PBXFileReference section */ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; - 2F435AE316A2CEF9DB2FCB44 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; - 45FB9682812B691627A14497 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; - 48A1E3B04AC3F3F79EE01940 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; - 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; - 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; - 97C146F21CF9000F007C117D /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - DC245C6D0FF6BF1D0B733C5B /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -56,24 +62,12 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - D290BBC2BCE42906E260DD85 /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - 54D582D77359D2F1CFEABAD6 /* Pods */ = { - isa = PBXGroup; - children = ( - 2F435AE316A2CEF9DB2FCB44 /* Pods-Runner.debug.xcconfig */, - 48A1E3B04AC3F3F79EE01940 /* Pods-Runner.release.xcconfig */, - 45FB9682812B691627A14497 /* Pods-Runner.profile.xcconfig */, - ); - name = Pods; - path = Pods; - sourceTree = ""; - }; 9740EEB11CF90186004384FC /* Flutter */ = { isa = PBXGroup; children = ( @@ -85,14 +79,21 @@ name = Flutter; sourceTree = ""; }; + 331C8082294A63A400263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C807B294A618700263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; 97C146E51CF9000F007C117D = { isa = PBXGroup; children = ( 9740EEB11CF90186004384FC /* Flutter */, 97C146F01CF9000F007C117D /* Runner */, 97C146EF1CF9000F007C117D /* Products */, - 54D582D77359D2F1CFEABAD6 /* Pods */, - 9B5485B940DE97CC1A7B761C /* Frameworks */, + 331C8082294A63A400263BE5 /* RunnerTests */, ); sourceTree = ""; }; @@ -100,6 +101,7 @@ isa = PBXGroup; children = ( 97C146EE1CF9000F007C117D /* Runner.app */, + 331C8081294A63A400263BE5 /* RunnerTests.xctest */, ); name = Products; sourceTree = ""; @@ -107,50 +109,49 @@ 97C146F01CF9000F007C117D /* Runner */ = { isa = PBXGroup; children = ( - 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */, - 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */, 97C146FA1CF9000F007C117D /* Main.storyboard */, 97C146FD1CF9000F007C117D /* Assets.xcassets */, 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, 97C147021CF9000F007C117D /* Info.plist */, - 97C146F11CF9000F007C117D /* Supporting Files */, 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, ); path = Runner; sourceTree = ""; }; - 97C146F11CF9000F007C117D /* Supporting Files */ = { - isa = PBXGroup; - children = ( - 97C146F21CF9000F007C117D /* main.m */, - ); - name = "Supporting Files"; - sourceTree = ""; - }; - 9B5485B940DE97CC1A7B761C /* Frameworks */ = { - isa = PBXGroup; - children = ( - DC245C6D0FF6BF1D0B733C5B /* Pods_Runner.framework */, - ); - name = Frameworks; - sourceTree = ""; - }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ + 331C8080294A63A400263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 331C807D294A63A400263BE5 /* Sources */, + 331C807E294A63A400263BE5 /* Frameworks */, + 331C807F294A63A400263BE5 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 331C8086294A63A400263BE5 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; 97C146ED1CF9000F007C117D /* Runner */ = { isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( - C781F89B47F2E7DB7E2B4898 /* [CP] Check Pods Manifest.lock */, 9740EEB61CF901F6004384FC /* Run Script */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, - 869AA06D856BCA582968F111 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -167,11 +168,17 @@ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { + BuildIndependentTargetsInParallel = YES; LastUpgradeCheck = 1430; ORGANIZATIONNAME = ""; TargetAttributes = { + 331C8080294A63A400263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 97C146ED1CF9000F007C117D; + }; 97C146ED1CF9000F007C117D = { CreatedOnToolsVersion = 7.3.1; + LastSwiftMigration = 1100; }; }; }; @@ -189,11 +196,19 @@ projectRoot = ""; targets = ( 97C146ED1CF9000F007C117D /* Runner */, + 331C8080294A63A400263BE5 /* RunnerTests */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ + 331C807F294A63A400263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; 97C146EC1CF9000F007C117D /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -224,23 +239,6 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; }; - 869AA06D856BCA582968F111 /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", - ); - name = "[CP] Embed Pods Frameworks"; - outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; @@ -256,43 +254,36 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; }; - C781F89B47F2E7DB7E2B4898 /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 331C807D294A63A400263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputFileListPaths = ( - ); - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; }; -/* End PBXShellScriptBuildPhase section */ - -/* Begin PBXSourcesBuildPhase section */ 97C146EA1CF9000F007C117D /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */, - 97C146F31CF9000F007C117D /* main.m in Sources */, + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ +/* Begin PBXTargetDependency section */ + 331C8086294A63A400263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 97C146ED1CF9000F007C117D /* Runner */; + targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + /* Begin PBXVariantGroup section */ 97C146FA1CF9000F007C117D /* Main.storyboard */ = { isa = PBXVariantGroup; @@ -368,27 +359,72 @@ baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; ENABLE_BITCODE = NO; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); - LIBRARY_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - PRODUCT_BUNDLE_IDENTIFIER = com.example.app; + PRODUCT_BUNDLE_IDENTIFIER = com.example.example; PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; }; name = Profile; }; + 331C8088294A63A400263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = AE0B7B92F70575B8D7E0D07E /* Pods-RunnerTests.debug.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.example.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Debug; + }; + 331C8089294A63A400263BE5 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 89B67EB44CE7B6631473024E /* Pods-RunnerTests.release.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.example.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Release; + }; + 331C808A294A63A400263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 640959BDD8F10B91D80A66BE /* Pods-RunnerTests.profile.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.example.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Profile; + }; 97C147031CF9000F007C117D /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -489,6 +525,8 @@ MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; }; @@ -499,23 +537,19 @@ baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; ENABLE_BITCODE = NO; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); - LIBRARY_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - PRODUCT_BUNDLE_IDENTIFIER = com.example.app; + PRODUCT_BUNDLE_IDENTIFIER = com.example.example; PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; }; name = Debug; @@ -525,23 +559,18 @@ baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; ENABLE_BITCODE = NO; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); - LIBRARY_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - PRODUCT_BUNDLE_IDENTIFIER = com.example.app; + PRODUCT_BUNDLE_IDENTIFIER = com.example.example; PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; }; name = Release; @@ -549,6 +578,16 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ + 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 331C8088294A63A400263BE5 /* Debug */, + 331C8089294A63A400263BE5 /* Release */, + 331C808A294A63A400263BE5 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index b52b2e69..87131a09 100644 --- a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -27,8 +27,6 @@ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" shouldUseLaunchSchemeArgsEnv = "YES"> - - - - + + + + + + - - - - diff --git a/example/ios/Runner/AppDelegate.h b/example/ios/Runner/AppDelegate.h deleted file mode 100644 index 36e21bbf..00000000 --- a/example/ios/Runner/AppDelegate.h +++ /dev/null @@ -1,6 +0,0 @@ -#import -#import - -@interface AppDelegate : FlutterAppDelegate - -@end diff --git a/example/ios/Runner/AppDelegate.m b/example/ios/Runner/AppDelegate.m deleted file mode 100644 index 70e83933..00000000 --- a/example/ios/Runner/AppDelegate.m +++ /dev/null @@ -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 diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png index 28c6bf03..7353c41e 100644 Binary files a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png index 2ccbfd96..797d452e 100644 Binary files a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png index f091b6b0..6ed2d933 100644 Binary files a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png index 4cde1211..4cd7b009 100644 Binary files a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png index d0ef06e7..fe730945 100644 Binary files a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png index dcdc2306..321773cd 100644 Binary files a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png index 2ccbfd96..797d452e 100644 Binary files a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png index c8f9ed8f..502f463a 100644 Binary files a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png index a6d6b860..0ec30343 100644 Binary files a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png index a6d6b860..0ec30343 100644 Binary files a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png index 75b2d164..e9f5fea2 100644 Binary files a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png index c4df70d3..84ac32ae 100644 Binary files a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png index 6a84f41e..8953cba0 100644 Binary files a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png index d0e1f585..0467bf12 100644 Binary files a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/example/ios/Runner/Info.plist b/example/ios/Runner/Info.plist index b8fc1f1d..be041944 100644 --- a/example/ios/Runner/Info.plist +++ b/example/ios/Runner/Info.plist @@ -4,6 +4,8 @@ CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Example CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier @@ -11,7 +13,7 @@ CFBundleInfoDictionaryVersion 6.0 CFBundleName - app + example CFBundlePackageType APPL CFBundleShortVersionString @@ -20,8 +22,6 @@ ???? CFBundleVersion $(FLUTTER_BUILD_NUMBER) - NSPhotoLibraryUsageDescription - Need to save image LSRequiresIPhoneOS UILaunchStoryboardName @@ -41,11 +41,17 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - UIViewControllerBasedStatusBarAppearance - CADisableMinimumFrameDurationOnPhone UIApplicationSupportsIndirectInputEvents + NSPhotoLibraryUsageDescription + We need permission to the photo library in order for inserting images in the text editor + NSCameraUsageDescription + We need permission to the camera in order for takeing a photos and record videos in the text editor + NSMicrophoneUsageDescription + We don't really need that permission + NSPhotoLibraryAddUsageDescription + We need this permission for saving the images in the editor diff --git a/example/ios/Runner/main.m b/example/ios/Runner/main.m deleted file mode 100644 index dff6597e..00000000 --- a/example/ios/Runner/main.m +++ /dev/null @@ -1,9 +0,0 @@ -#import -#import -#import "AppDelegate.h" - -int main(int argc, char* argv[]) { - @autoreleasepool { - return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); - } -} diff --git a/example/ios/RunnerTests/RunnerTests.swift b/example/ios/RunnerTests/RunnerTests.swift new file mode 100644 index 00000000..86a7c3b1 --- /dev/null +++ b/example/ios/RunnerTests/RunnerTests.swift @@ -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. + } + +} diff --git a/example/lib/gen/assets.gen.dart b/example/lib/gen/assets.gen.dart new file mode 100644 index 00000000..3075816e --- /dev/null +++ b/example/lib/gen/assets.gen.dart @@ -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 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? 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; +} diff --git a/example/lib/gen/fonts.gen.dart b/example/lib/gen/fonts.gen.dart new file mode 100644 index 00000000..f61cfa18 --- /dev/null +++ b/example/lib/gen/fonts.gen.dart @@ -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'; +} diff --git a/example/lib/logic/empty.dart b/example/lib/logic/empty.dart new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/example/lib/logic/empty.dart @@ -0,0 +1 @@ + diff --git a/example/lib/main.dart b/example/lib/main.dart index c4a44613..e263fb25 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -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 MaterialApp( - debugShowCheckedModeBanner: false, - title: 'Quill Demo', - theme: ThemeData( - primarySwatch: Colors.blue, - visualDensity: VisualDensity.adaptivePlatformDensity, - ), - localizationsDelegates: [ - GlobalMaterialLocalizations.delegate, - GlobalWidgetsLocalizations.delegate, - GlobalCupertinoLocalizations.delegate, - ], - supportedLocales: [ - const Locale('en', 'US'), - const Locale('zh', 'HK'), + return MultiBlocProvider( + providers: [ + BlocProvider( + create: (context) => SettingsCubit(), + ), ], - home: HomePage(), + child: BlocBuilder( + builder: (context, state) { + return MaterialApp( + title: 'Flutter Quill Demo', + theme: ThemeData.light(useMaterial3: true), + darkTheme: ThemeData.dark(useMaterial3: true), + themeMode: state.themeMode, + debugShowCheckedModeBanner: false, + localizationsDelegates: const [ + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + // FlutterQuillLocalizations.delegate, + ], + 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, + ); + }, + ), + ); + }, + ), ); } } diff --git a/example/lib/pages/home_page.dart b/example/lib/pages/home_page.dart deleted file mode 100644 index fffc4abb..00000000 --- a/example/lib/pages/home_page.dart +++ /dev/null @@ -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 { - late final QuillController _controller; - late final Future _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 _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: [ - 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 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 _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 _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 _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 _selectMediaPickSetting(BuildContext context) => - showDialog( - 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 _selectCameraPickSetting(BuildContext context) => - showDialog( - 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 _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, - ); - } -} diff --git a/example/lib/pages/read_only_page.dart b/example/lib/pages/read_only_page.dart deleted file mode 100644 index 5693f6b3..00000000 --- a/example/lib/pages/read_only_page.dart +++ /dev/null @@ -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 { - 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; - }); - } -} diff --git a/example/lib/presentation/extensions/scaffold_messenger.dart b/example/lib/presentation/extensions/scaffold_messenger.dart new file mode 100644 index 00000000..c46783de --- /dev/null +++ b/example/lib/presentation/extensions/scaffold_messenger.dart @@ -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); +} diff --git a/example/lib/presentation/home/widgets/example_item.dart b/example/lib/presentation/home/widgets/example_item.dart new file mode 100644 index 00000000..5c1badd3 --- /dev/null +++ b/example/lib/presentation/home/widgets/example_item.dart @@ -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, + ), + ), + ], + ), + ), + ), + ), + ), + ], + ); + } +} diff --git a/example/lib/presentation/home/widgets/home_screen.dart b/example/lib/presentation/home/widgets/home_screen.dart new file mode 100644 index 00000000..2ea2a8c7 --- /dev/null +++ b/example/lib/presentation/home/widgets/home_screen.dart @@ -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(), + ), + ), + ), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/example/lib/widgets/time_stamp_embed_widget.dart b/example/lib/presentation/quill/embeds/timestamp_embed.dart similarity index 80% rename from example/lib/widgets/time_stamp_embed_widget.dart rename to example/lib/presentation/quill/embeds/timestamp_embed.dart index 968cb441..bffe16c6 100644 --- a/example/lib/widgets/time_stamp_embed_widget.dart +++ b/example/lib/presentation/quill/embeds/timestamp_embed.dart @@ -1,6 +1,7 @@ -import 'dart:convert'; +import 'dart:convert' show jsonDecode, jsonEncode; -import 'package:flutter/material.dart'; +import 'package:flutter/material.dart' show Icons; +import 'package:flutter/widgets.dart'; import 'package:flutter_quill/flutter_quill.dart'; class TimeStampEmbed extends Embeddable { @@ -21,8 +22,8 @@ class TimeStampEmbedBuilderWidget extends EmbedBuilder { String get key => 'timeStamp'; @override - String toPlainText(Embed embed) { - return embed.value.data; + String toPlainText(Embed node) { + return node.value.data; } @override diff --git a/example/lib/presentation/quill/quill_editor.dart b/example/lib/presentation/quill/quill_editor.dart new file mode 100644 index 00000000..133f9997 --- /dev/null +++ b/example/lib/presentation/quill/quill_editor.dart @@ -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, + ); + }, + ), + ); + } +} diff --git a/example/lib/presentation/quill/quill_screen.dart b/example/lib/presentation/quill/quill_screen.dart new file mode 100644 index 00000000..aab91c83 --- /dev/null +++ b/example/lib/presentation/quill/quill_screen.dart @@ -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 createState() => _QuillScreenState(); +} + +class _QuillScreenState extends State { + 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), + ), + ); + } +} diff --git a/example/lib/presentation/quill/quill_toolbar.dart b/example/lib/presentation/quill/quill_toolbar.dart new file mode 100644 index 00000000..b0257dc1 --- /dev/null +++ b/example/lib/presentation/quill/quill_toolbar.dart @@ -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 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 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 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( + 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, + ), + ), + ), + ), + ); + }, + ); + } +} diff --git a/example/lib/presentation/quill/samples/quill_default_sample.dart b/example/lib/presentation/quill/samples/quill_default_sample.dart new file mode 100644 index 00000000..618775cf --- /dev/null +++ b/example/lib/presentation/quill/samples/quill_default_sample.dart @@ -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'} +]; diff --git a/example/lib/presentation/quill/samples/quill_images_sample.dart b/example/lib/presentation/quill/samples/quill_images_sample.dart new file mode 100644 index 00000000..b483e733 --- /dev/null +++ b/example/lib/presentation/quill/samples/quill_images_sample.dart @@ -0,0 +1,72 @@ +import '../../../gen/assets.gen.dart'; + +final quillImagesSample = [ + {'insert': 'This is an asset image: \n'}, + {'insert': '\n'}, + { + 'insert': {'image': Assets.images.screenshot1.path}, + 'attributes': { + 'width': '100', + 'height': '100', + 'style': 'width:500px; height:350px;' + } + }, + {'insert': '\n'}, + {'insert': 'Here is a network image: \n'}, + {'insert': '\n'}, + { + 'insert': { + 'image': + 'https://helpx.adobe.com/content/dam/help/en/photoshop/using/convert-color-image-black-white/jcr_content/main-pars/before_and_after/image-before/Landscape-Color.jpg' + }, + 'attributes': { + 'width': '100', + 'height': '100', + 'style': 'width:250px; height:250px;' + } + }, + {'insert': '\n'}, + {'insert': '\n'}, + { + 'insert': + '\nThe image above have 250px width and height in the css style attribute which will be used for web, and 100 width and height that is in the attributes which will be used for desktop and mobile\n' + }, + {'insert': '\n'}, + { + 'insert': { + 'image': + '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' + }, + 'attributes': { + 'width': '100', + 'height': '100', + 'style': 'width:250px; height:250px;' + } + }, + {'insert': '\n'}, + { + 'insert': + '\nThe source of the above image is image base 64 directly without `data:image/png;base64,` in the start' + }, + {'insert': '\n'}, + {'insert': '\n'}, + {'insert': ''}, + { + 'insert': { + 'image': + 'data:image/png;base64,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' + }, + 'attributes': { + 'width': '100', + 'height': '100', + 'style': 'width:250px; height:250px;' + } + }, + {'insert': '\n'}, + {'insert': '\n'}, + { + 'insert': + 'The source of the above image is also image base 64 but this time it start with `data:image/png;base64,`' + }, + {'insert': '\n'}, +]; diff --git a/example/lib/presentation/quill/samples/quill_text_sample.dart b/example/lib/presentation/quill/samples/quill_text_sample.dart new file mode 100644 index 00000000..4e047db9 --- /dev/null +++ b/example/lib/presentation/quill/samples/quill_text_sample.dart @@ -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'} +]; diff --git a/example/lib/presentation/quill/samples/quill_videos_sample.dart b/example/lib/presentation/quill/samples/quill_videos_sample.dart new file mode 100644 index 00000000..90f0243e --- /dev/null +++ b/example/lib/presentation/quill/samples/quill_videos_sample.dart @@ -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'}, +]; diff --git a/example/lib/presentation/settings/cubit/settings_cubit.dart b/example/lib/presentation/settings/cubit/settings_cubit.dart new file mode 100644 index 00000000..11bd2042 --- /dev/null +++ b/example/lib/presentation/settings/cubit/settings_cubit.dart @@ -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 with HydratedMixin { + SettingsCubit() : super(const SettingsState()); + + void updateSettings(SettingsState newSettingsState) { + emit(newSettingsState); + } + + @override + SettingsState? fromJson(Map json) { + return SettingsState.fromJson(json); + } + + @override + Map? toJson(SettingsState state) { + return state.toJson(); + } +} diff --git a/example/lib/presentation/settings/cubit/settings_cubit.freezed.dart b/example/lib/presentation/settings/cubit/settings_cubit.freezed.dart new file mode 100644 index 00000000..1a0795df --- /dev/null +++ b/example/lib/presentation/settings/cubit/settings_cubit.freezed.dart @@ -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 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 json) { + return _SettingsState.fromJson(json); +} + +/// @nodoc +mixin _$SettingsState { + ThemeMode get themeMode => throw _privateConstructorUsedError; + DefaultScreen get defaultScreen => throw _privateConstructorUsedError; + bool get useCustomQuillToolbar => throw _privateConstructorUsedError; + + Map toJson() => throw _privateConstructorUsedError; + @JsonKey(ignore: true) + $SettingsStateCopyWith 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 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 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 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; +} diff --git a/example/lib/presentation/settings/cubit/settings_cubit.g.dart b/example/lib/presentation/settings/cubit/settings_cubit.g.dart new file mode 100644 index 00000000..fe3fd931 --- /dev/null +++ b/example/lib/presentation/settings/cubit/settings_cubit.g.dart @@ -0,0 +1,40 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'settings_cubit.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$SettingsStateImpl _$$SettingsStateImplFromJson(Map json) => + _$SettingsStateImpl( + themeMode: $enumDecodeNullable(_$ThemeModeEnumMap, json['themeMode']) ?? + ThemeMode.system, + defaultScreen: + $enumDecodeNullable(_$DefaultScreenEnumMap, json['defaultScreen']) ?? + DefaultScreen.home, + useCustomQuillToolbar: json['useCustomQuillToolbar'] as bool? ?? false, + ); + +Map _$$SettingsStateImplToJson(_$SettingsStateImpl instance) => + { + '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', +}; diff --git a/example/lib/presentation/settings/cubit/settings_state.dart b/example/lib/presentation/settings/cubit/settings_state.dart new file mode 100644 index 00000000..67e5adae --- /dev/null +++ b/example/lib/presentation/settings/cubit/settings_state.dart @@ -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 json) => + _$SettingsStateFromJson(json); +} diff --git a/example/lib/presentation/settings/widgets/settings_screen.dart b/example/lib/presentation/settings/widgets/settings_screen.dart new file mode 100644 index 00000000..49ffa456 --- /dev/null +++ b/example/lib/presentation/settings/widgets/settings_screen.dart @@ -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( + builder: (context, state) { + return ListView( + children: [ + CheckboxListTile.adaptive( + value: isDark, + onChanged: (value) { + final isNewValueDark = value ?? false; + context.read().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(); + final newDefaultScreen = + await showAdaptiveDialog( + 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().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), + ), + ], + ); + }, + ), + ); + } +} diff --git a/example/lib/presentation/shared/widgets/dialog_action.dart b/example/lib/presentation/shared/widgets/dialog_action.dart new file mode 100644 index 00000000..0761e066 --- /dev/null +++ b/example/lib/presentation/shared/widgets/dialog_action.dart @@ -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, + ); + } +} diff --git a/example/lib/presentation/shared/widgets/home_screen_button.dart b/example/lib/presentation/shared/widgets/home_screen_button.dart new file mode 100644 index 00000000..b1ac053a --- /dev/null +++ b/example/lib/presentation/shared/widgets/home_screen_button.dart @@ -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.updateSettings( + settingsCubit.state.copyWith( + defaultScreen: DefaultScreen.home, + ), + ); + }, + icon: const Icon(Icons.home), + tooltip: 'Set the default to home screen', + ); + } +} diff --git a/example/lib/universal_ui/fake_ui.dart b/example/lib/universal_ui/fake_ui.dart deleted file mode 100644 index 1711ad5f..00000000 --- a/example/lib/universal_ui/fake_ui.dart +++ /dev/null @@ -1,3 +0,0 @@ -class PlatformViewRegistry { - static void registerViewFactory(String viewId, dynamic cb) {} -} diff --git a/example/lib/universal_ui/real_ui.dart b/example/lib/universal_ui/real_ui.dart deleted file mode 100644 index 6c1072fc..00000000 --- a/example/lib/universal_ui/real_ui.dart +++ /dev/null @@ -1,7 +0,0 @@ -import 'dart:ui' if (dart.library.html) 'dart:ui_web' as ui; - -class PlatformViewRegistry { - static void registerViewFactory(String viewId, dynamic cb) { - ui.platformViewRegistry.registerViewFactory(viewId, cb); - } -} diff --git a/example/lib/universal_ui/universal_ui.dart b/example/lib/universal_ui/universal_ui.dart deleted file mode 100644 index 421725e2..00000000 --- a/example/lib/universal_ui/universal_ui.dart +++ /dev/null @@ -1,114 +0,0 @@ -library universal_ui; - -import 'package:flutter/cupertino.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter_quill/flutter_quill.dart'; -import 'package:flutter_quill_extensions/flutter_quill_extensions.dart'; -import 'package:universal_html/html.dart' as html; -import 'package:youtube_player_flutter/youtube_player_flutter.dart'; - -import '../widgets/responsive_widget.dart'; -import 'fake_ui.dart' if (dart.library.html) 'real_ui.dart' as ui_instance; - -class PlatformViewRegistryFix { - void registerViewFactory(dynamic x, dynamic y) { - if (kIsWeb) { - ui_instance.PlatformViewRegistry.registerViewFactory( - x, - y, - ); - } - } -} - -class UniversalUI { - PlatformViewRegistryFix platformViewRegistry = PlatformViewRegistryFix(); -} - -var ui = UniversalUI(); - -class ImageEmbedBuilderWeb extends EmbedBuilder { - @override - String get key => BlockEmbed.imageType; - - @override - Widget build( - BuildContext context, - QuillController controller, - Embed node, - bool readOnly, - bool inline, - TextStyle textStyle, - ) { - final imageUrl = node.value.data; - if (isImageBase64(imageUrl)) { - // TODO: handle imageUrl of base64 - return const SizedBox(); - } - final size = MediaQuery.sizeOf(context); - UniversalUI().platformViewRegistry.registerViewFactory(imageUrl, (viewId) { - return html.ImageElement() - ..src = imageUrl - ..style.height = 'auto' - ..style.width = 'auto'; - }); - return Padding( - padding: EdgeInsets.only( - right: ResponsiveWidget.isMediumScreen(context) - ? size.width * 0.5 - : (ResponsiveWidget.isLargeScreen(context)) - ? size.width * 0.75 - : size.width * 0.2, - ), - child: SizedBox( - height: MediaQuery.sizeOf(context).height * 0.45, - child: HtmlElementView( - viewType: imageUrl, - ), - ), - ); - } -} - -class VideoEmbedBuilderWeb extends EmbedBuilder { - @override - String get key => BlockEmbed.videoType; - - @override - Widget build( - BuildContext context, - QuillController controller, - Embed node, - bool readOnly, - bool inline, - TextStyle textStyle, - ) { - var videoUrl = node.value.data; - if (videoUrl.contains('youtube.com') || videoUrl.contains('youtu.be')) { - final youtubeID = YoutubePlayer.convertUrlToId(videoUrl); - if (youtubeID != null) { - videoUrl = 'https://www.youtube.com/embed/$youtubeID'; - } - } - - UniversalUI().platformViewRegistry.registerViewFactory( - videoUrl, - (id) => html.IFrameElement() - ..width = MediaQuery.sizeOf(context).width.toString() - ..height = MediaQuery.sizeOf(context).height.toString() - ..src = videoUrl - ..style.border = 'none'); - - return SizedBox( - height: 500, - child: HtmlElementView( - viewType: videoUrl, - ), - ); - } -} - -List get defaultEmbedBuildersWeb => [ - ImageEmbedBuilderWeb(), - VideoEmbedBuilderWeb(), - ]; diff --git a/example/lib/widgets/demo_scaffold.dart b/example/lib/widgets/demo_scaffold.dart deleted file mode 100644 index fb96b01c..00000000 --- a/example/lib/widgets/demo_scaffold.dart +++ /dev/null @@ -1,140 +0,0 @@ -import 'dart:convert'; -import 'dart:io' show Platform; - -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/flutter_quill.dart'; -import 'package:flutter_quill_extensions/flutter_quill_extensions.dart'; -import 'package:path_provider/path_provider.dart'; - -typedef DemoContentBuilder = Widget Function( - BuildContext context, QuillController? controller); - -// Common scaffold for all examples. -class DemoScaffold extends StatefulWidget { - const DemoScaffold({ - required this.documentFilename, - required this.builder, - this.actions, - this.showToolbar = true, - this.floatingActionButton, - Key? key, - }) : super(key: key); - - /// Filename of the document to load into the editor. - final String documentFilename; - final DemoContentBuilder builder; - final List? actions; - final Widget? floatingActionButton; - final bool showToolbar; - - @override - _DemoScaffoldState createState() => _DemoScaffoldState(); -} - -class _DemoScaffoldState extends State { - final _scaffoldKey = GlobalKey(); - QuillController? _controller; - - bool _loading = false; - - @override - void didChangeDependencies() { - super.didChangeDependencies(); - if (_controller == null && !_loading) { - _loading = true; - _loadFromAssets(); - } - } - - @override - void dispose() { - _controller?.dispose(); - super.dispose(); - } - - Future _loadFromAssets() async { - try { - final result = - await rootBundle.loadString('assets/${widget.documentFilename}'); - final doc = Document.fromJson(jsonDecode(result)); - setState(() { - _controller = QuillController( - document: doc, selection: const TextSelection.collapsed(offset: 0)); - _loading = false; - }); - } catch (error) { - final doc = Document()..insert(0, 'Empty asset'); - setState(() { - _controller = QuillController( - document: doc, selection: const TextSelection.collapsed(offset: 0)); - _loading = false; - }); - } - } - - Future openFileSystemPickerForDesktop(BuildContext context) async { - return await FilesystemPicker.open( - context: context, - rootDirectory: await getApplicationDocumentsDirectory(), - fsType: FilesystemType.file, - fileTileSelectMode: FileTileSelectMode.wholeTile, - ); - } - - QuillToolbar get quillToolbar { - if (_isDesktop()) { - return QuillToolbar( - configurations: QuillToolbarConfigurations( - embedButtons: FlutterQuillEmbeds.buttons( - filePickImpl: openFileSystemPickerForDesktop, - ), - ), - ); - } - return QuillToolbar( - configurations: QuillToolbarConfigurations( - embedButtons: FlutterQuillEmbeds.buttons(), - ), - ); - } - - @override - Widget build(BuildContext context) { - if (_controller == null) { - return const Scaffold(body: Center(child: Text('Loading...'))); - } - final actions = widget.actions ?? []; - - return QuillProvider( - configurations: QuillConfigurations(controller: _controller!), - child: Scaffold( - key: _scaffoldKey, - appBar: AppBar( - elevation: 0, - backgroundColor: Theme.of(context).canvasColor, - centerTitle: false, - titleSpacing: 0, - leading: IconButton( - icon: Icon( - Icons.chevron_left, - color: Colors.grey.shade800, - size: 18, - ), - onPressed: () => Navigator.pop(context), - ), - title: _loading || !widget.showToolbar ? null : quillToolbar, - actions: actions, - ), - floatingActionButton: widget.floatingActionButton, - body: _loading - ? const Center(child: Text('Loading...')) - : widget.builder(context, _controller), - ), - ); - } - - bool _isDesktop() => !kIsWeb && !Platform.isAndroid && !Platform.isIOS; -} diff --git a/example/lib/widgets/responsive_widget.dart b/example/lib/widgets/responsive_widget.dart deleted file mode 100644 index f9de4027..00000000 --- a/example/lib/widgets/responsive_widget.dart +++ /dev/null @@ -1,43 +0,0 @@ -import 'package:flutter/material.dart'; - -class ResponsiveWidget extends StatelessWidget { - const ResponsiveWidget({ - required this.largeScreen, - this.mediumScreen, - this.smallScreen, - Key? key, - }) : super(key: key); - - final Widget largeScreen; - final Widget? mediumScreen; - final Widget? smallScreen; - - static bool isSmallScreen(BuildContext context) { - return MediaQuery.sizeOf(context).width < 800; - } - - static bool isLargeScreen(BuildContext context) { - return MediaQuery.sizeOf(context).width > 1200; - } - - static bool isMediumScreen(BuildContext context) { - return MediaQuery.sizeOf(context).width >= 800 && - MediaQuery.sizeOf(context).width <= 1200; - } - - @override - Widget build(BuildContext context) { - return LayoutBuilder( - builder: (context, constraints) { - if (constraints.maxWidth > 1200) { - return largeScreen; - } else if (constraints.maxWidth <= 1200 && - constraints.maxWidth >= 800) { - return mediumScreen ?? largeScreen; - } else { - return smallScreen ?? largeScreen; - } - }, - ); - } -} diff --git a/example/linux/.gitignore b/example/linux/.gitignore index c7ea17fc..d3896c98 100644 --- a/example/linux/.gitignore +++ b/example/linux/.gitignore @@ -1 +1 @@ -flutter/ephemeral +flutter/ephemeral diff --git a/example/linux/CMakeLists.txt b/example/linux/CMakeLists.txt index 6ec85464..d67bd4e0 100644 --- a/example/linux/CMakeLists.txt +++ b/example/linux/CMakeLists.txt @@ -1,106 +1,139 @@ -cmake_minimum_required(VERSION 3.10) -project(runner LANGUAGES CXX) - -set(BINARY_NAME "app") -set(APPLICATION_ID "com.example.app") - -cmake_policy(SET CMP0063 NEW) - -set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") - -# Configure build options. -if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) - set(CMAKE_BUILD_TYPE "Debug" CACHE - STRING "Flutter build mode" FORCE) - set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS - "Debug" "Profile" "Release") -endif() - -# Compilation settings that should be applied to most targets. -function(APPLY_STANDARD_SETTINGS TARGET) - target_compile_features(${TARGET} PUBLIC cxx_std_14) - target_compile_options(${TARGET} PRIVATE -Wall -Werror) - target_compile_options(${TARGET} PRIVATE "$<$>:-O3>") - target_compile_definitions(${TARGET} PRIVATE "$<$>:NDEBUG>") -endfunction() - -set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") - -# Flutter library and tool build rules. -add_subdirectory(${FLUTTER_MANAGED_DIR}) - -# System-level dependencies. -find_package(PkgConfig REQUIRED) -pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) - -add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}") - -# Application build -add_executable(${BINARY_NAME} - "main.cc" - "my_application.cc" - "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" -) -apply_standard_settings(${BINARY_NAME}) -target_link_libraries(${BINARY_NAME} PRIVATE flutter) -target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) -add_dependencies(${BINARY_NAME} flutter_assemble) -# Only the install-generated bundle's copy of the executable will launch -# correctly, since the resources must in the right relative locations. To avoid -# people trying to run the unbundled copy, put it in a subdirectory instead of -# the default top-level location. -set_target_properties(${BINARY_NAME} - PROPERTIES - RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run" -) - -# Generated plugin build rules, which manage building the plugins and adding -# them to the application. -include(flutter/generated_plugins.cmake) - - -# === Installation === -# By default, "installing" just makes a relocatable bundle in the build -# directory. -set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle") -if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) - set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) -endif() - -# Start with a clean build bundle directory every time. -install(CODE " - file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\") - " COMPONENT Runtime) - -set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") -set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib") - -install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" - COMPONENT Runtime) - -install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" - COMPONENT Runtime) - -install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" - COMPONENT Runtime) - -if(PLUGIN_BUNDLED_LIBRARIES) - install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" - DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" - COMPONENT Runtime) -endif() - -# Fully re-copy the assets directory on each build to avoid having stale files -# from a previous install. -set(FLUTTER_ASSET_DIR_NAME "flutter_assets") -install(CODE " - file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") - " COMPONENT Runtime) -install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" - DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) - -# Install the AOT library on non-Debug builds only. -if(NOT CMAKE_BUILD_TYPE MATCHES "Debug") - install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" - COMPONENT Runtime) -endif() +# Project-level configuration. +cmake_minimum_required(VERSION 3.10) +project(runner LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "example") +# The unique GTK application identifier for this application. See: +# https://wiki.gnome.org/HowDoI/ChooseApplicationID +set(APPLICATION_ID "com.example.example") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(SET CMP0063 NEW) + +# Load bundled libraries from the lib/ directory relative to the binary. +set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") + +# Root filesystem for cross-building. +if(FLUTTER_TARGET_PLATFORM_SYSROOT) + set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) + set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) +endif() + +# Define build configuration options. +if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") +endif() + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_14) + target_compile_options(${TARGET} PRIVATE -Wall -Werror) + target_compile_options(${TARGET} PRIVATE "$<$>:-O3>") + target_compile_definitions(${TARGET} PRIVATE "$<$>:NDEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) + +add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}") + +# Define the application target. To change its name, change BINARY_NAME above, +# not the value here, or `flutter run` will no longer work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} + "main.cc" + "my_application.cc" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add dependency libraries. Add any application-specific dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter) +target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) + +# Only the install-generated bundle's copy of the executable will launch +# correctly, since the resources must in the right relative locations. To avoid +# people trying to run the unbundled copy, put it in a subdirectory instead of +# the default top-level location. +set_target_properties(${BINARY_NAME} + PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run" +) + + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# By default, "installing" just makes a relocatable bundle in the build +# directory. +set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle") +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +# Start with a clean build bundle directory every time. +install(CODE " + file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\") + " COMPONENT Runtime) + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES}) + install(FILES "${bundled_library}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endforeach(bundled_library) + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +if(NOT CMAKE_BUILD_TYPE MATCHES "Debug") + install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() diff --git a/example/linux/flutter/CMakeLists.txt b/example/linux/flutter/CMakeLists.txt index 40425dce..d5bd0164 100644 --- a/example/linux/flutter/CMakeLists.txt +++ b/example/linux/flutter/CMakeLists.txt @@ -1,91 +1,88 @@ -cmake_minimum_required(VERSION 3.10) - -set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") - -# Configuration provided via flutter tool. -include(${EPHEMERAL_DIR}/generated_config.cmake) - -# TODO: Move the rest of this into files in ephemeral. See -# https://github.com/flutter/flutter/issues/57146. - -# Serves the same purpose as list(TRANSFORM ... PREPEND ...), -# which isn't available in 3.10. -function(list_prepend LIST_NAME PREFIX) - set(NEW_LIST "") - foreach(element ${${LIST_NAME}}) - list(APPEND NEW_LIST "${PREFIX}${element}") - endforeach(element) - set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE) -endfunction() - -# === Flutter Library === -# System-level dependencies. -find_package(PkgConfig REQUIRED) -pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) -pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0) -pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0) -pkg_check_modules(BLKID REQUIRED IMPORTED_TARGET blkid) -pkg_check_modules(LZMA REQUIRED IMPORTED_TARGET liblzma) - -set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so") - -# Published to parent scope for install step. -set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) -set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) -set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) -set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE) - -list(APPEND FLUTTER_LIBRARY_HEADERS - "fl_basic_message_channel.h" - "fl_binary_codec.h" - "fl_binary_messenger.h" - "fl_dart_project.h" - "fl_engine.h" - "fl_json_message_codec.h" - "fl_json_method_codec.h" - "fl_message_codec.h" - "fl_method_call.h" - "fl_method_channel.h" - "fl_method_codec.h" - "fl_method_response.h" - "fl_plugin_registrar.h" - "fl_plugin_registry.h" - "fl_standard_message_codec.h" - "fl_standard_method_codec.h" - "fl_string_codec.h" - "fl_value.h" - "fl_view.h" - "flutter_linux.h" -) -list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/") -add_library(flutter INTERFACE) -target_include_directories(flutter INTERFACE - "${EPHEMERAL_DIR}" -) -target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}") -target_link_libraries(flutter INTERFACE - PkgConfig::GTK - PkgConfig::GLIB - PkgConfig::GIO - PkgConfig::BLKID - PkgConfig::LZMA -) -add_dependencies(flutter flutter_assemble) - -# === Flutter tool backend === -# _phony_ is a non-existent file to force this command to run every time, -# since currently there's no way to get a full input/output list from the -# flutter tool. -add_custom_command( - OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} - ${CMAKE_CURRENT_BINARY_DIR}/_phony_ - COMMAND ${CMAKE_COMMAND} -E env - ${FLUTTER_TOOL_ENVIRONMENT} - "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh" - ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE} - VERBATIM -) -add_custom_target(flutter_assemble DEPENDS - "${FLUTTER_LIBRARY}" - ${FLUTTER_LIBRARY_HEADERS} -) +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.10) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. + +# Serves the same purpose as list(TRANSFORM ... PREPEND ...), +# which isn't available in 3.10. +function(list_prepend LIST_NAME PREFIX) + set(NEW_LIST "") + foreach(element ${${LIST_NAME}}) + list(APPEND NEW_LIST "${PREFIX}${element}") + endforeach(element) + set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE) +endfunction() + +# === Flutter Library === +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) +pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0) +pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0) + +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "fl_basic_message_channel.h" + "fl_binary_codec.h" + "fl_binary_messenger.h" + "fl_dart_project.h" + "fl_engine.h" + "fl_json_message_codec.h" + "fl_json_method_codec.h" + "fl_message_codec.h" + "fl_method_call.h" + "fl_method_channel.h" + "fl_method_codec.h" + "fl_method_response.h" + "fl_plugin_registrar.h" + "fl_plugin_registry.h" + "fl_standard_message_codec.h" + "fl_standard_method_codec.h" + "fl_string_codec.h" + "fl_value.h" + "fl_view.h" + "flutter_linux.h" +) +list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}") +target_link_libraries(flutter INTERFACE + PkgConfig::GTK + PkgConfig::GLIB + PkgConfig::GIO +) +add_dependencies(flutter flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CMAKE_CURRENT_BINARY_DIR}/_phony_ + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh" + ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE} + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} +) diff --git a/example/linux/flutter/generated_plugin_registrant.cc b/example/linux/flutter/generated_plugin_registrant.cc index 158f759a..fe311fa2 100644 --- a/example/linux/flutter/generated_plugin_registrant.cc +++ b/example/linux/flutter/generated_plugin_registrant.cc @@ -6,11 +6,15 @@ #include "generated_plugin_registrant.h" +#include #include #include #include void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) desktop_drop_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "DesktopDropPlugin"); + desktop_drop_plugin_register_with_registrar(desktop_drop_registrar); g_autoptr(FlPluginRegistrar) file_selector_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin"); file_selector_plugin_register_with_registrar(file_selector_linux_registrar); diff --git a/example/linux/flutter/generated_plugins.cmake b/example/linux/flutter/generated_plugins.cmake index 93c755ee..3f7f250e 100644 --- a/example/linux/flutter/generated_plugins.cmake +++ b/example/linux/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + desktop_drop file_selector_linux pasteboard url_launcher_linux diff --git a/example/linux/main.cc b/example/linux/main.cc index 4340ffc1..e7c5c543 100644 --- a/example/linux/main.cc +++ b/example/linux/main.cc @@ -1,6 +1,6 @@ -#include "my_application.h" - -int main(int argc, char** argv) { - g_autoptr(MyApplication) app = my_application_new(); - return g_application_run(G_APPLICATION(app), argc, argv); -} +#include "my_application.h" + +int main(int argc, char** argv) { + g_autoptr(MyApplication) app = my_application_new(); + return g_application_run(G_APPLICATION(app), argc, argv); +} diff --git a/example/linux/my_application.cc b/example/linux/my_application.cc index 5f64f84a..0ba8f430 100644 --- a/example/linux/my_application.cc +++ b/example/linux/my_application.cc @@ -1,104 +1,104 @@ -#include "my_application.h" - -#include -#ifdef GDK_WINDOWING_X11 -#include -#endif - -#include "flutter/generated_plugin_registrant.h" - -struct _MyApplication { - GtkApplication parent_instance; - char** dart_entrypoint_arguments; -}; - -G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) - -// Implements GApplication::activate. -static void my_application_activate(GApplication* application) { - MyApplication* self = MY_APPLICATION(application); - GtkWindow* window = - GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); - - // Use a header bar when running in GNOME as this is the common style used - // by applications and is the setup most users will be using (e.g. Ubuntu - // desktop). - // If running on X and not using GNOME then just use a traditional title bar - // in case the window manager does more exotic layout, e.g. tiling. - // If running on Wayland assume the header bar will work (may need changing - // if future cases occur). - gboolean use_header_bar = FALSE; -#ifdef GDK_WINDOWING_X11 - GdkScreen *screen = gtk_window_get_screen(window); - if (GDK_IS_X11_SCREEN(screen)) { - const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen); - if (g_strcmp0(wm_name, "GNOME Shell") != 0) { - use_header_bar = FALSE; - } - } -#endif - if (use_header_bar) { - GtkHeaderBar *header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); - gtk_widget_show(GTK_WIDGET(header_bar)); - gtk_header_bar_set_title(header_bar, "app"); - gtk_header_bar_set_show_close_button(header_bar, TRUE); - gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); - } - else { - gtk_window_set_title(window, "app"); - } - - gtk_window_set_default_size(window, 1280, 720); - gtk_widget_show(GTK_WIDGET(window)); - - g_autoptr(FlDartProject) project = fl_dart_project_new(); - fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments); - - FlView* view = fl_view_new(project); - gtk_widget_show(GTK_WIDGET(view)); - gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); - - fl_register_plugins(FL_PLUGIN_REGISTRY(view)); - - gtk_widget_grab_focus(GTK_WIDGET(view)); -} - -// Implements GApplication::local_command_line. -static gboolean my_application_local_command_line(GApplication* application, gchar ***arguments, int *exit_status) { - MyApplication* self = MY_APPLICATION(application); - // Strip out the first argument as it is the binary name. - self->dart_entrypoint_arguments = g_strdupv(*arguments + 1); - - g_autoptr(GError) error = nullptr; - if (!g_application_register(application, nullptr, &error)) { - g_warning("Failed to register: %s", error->message); - *exit_status = 1; - return TRUE; - } - - g_application_activate(application); - *exit_status = 0; - - return TRUE; -} - -// Implements GObject::dispose. -static void my_application_dispose(GObject *object) { - MyApplication* self = MY_APPLICATION(object); - g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev); - G_OBJECT_CLASS(my_application_parent_class)->dispose(object); -} - -static void my_application_class_init(MyApplicationClass* klass) { - G_APPLICATION_CLASS(klass)->activate = my_application_activate; - G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line; - G_OBJECT_CLASS(klass)->dispose = my_application_dispose; -} - -static void my_application_init(MyApplication* self) {} - -MyApplication* my_application_new() { - return MY_APPLICATION(g_object_new(my_application_get_type(), - "application-id", APPLICATION_ID, - nullptr)); -} +#include "my_application.h" + +#include +#ifdef GDK_WINDOWING_X11 +#include +#endif + +#include "flutter/generated_plugin_registrant.h" + +struct _MyApplication { + GtkApplication parent_instance; + char** dart_entrypoint_arguments; +}; + +G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) + +// Implements GApplication::activate. +static void my_application_activate(GApplication* application) { + MyApplication* self = MY_APPLICATION(application); + GtkWindow* window = + GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); + + // Use a header bar when running in GNOME as this is the common style used + // by applications and is the setup most users will be using (e.g. Ubuntu + // desktop). + // If running on X and not using GNOME then just use a traditional title bar + // in case the window manager does more exotic layout, e.g. tiling. + // If running on Wayland assume the header bar will work (may need changing + // if future cases occur). + gboolean use_header_bar = TRUE; +#ifdef GDK_WINDOWING_X11 + GdkScreen* screen = gtk_window_get_screen(window); + if (GDK_IS_X11_SCREEN(screen)) { + const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen); + if (g_strcmp0(wm_name, "GNOME Shell") != 0) { + use_header_bar = FALSE; + } + } +#endif + if (use_header_bar) { + GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); + gtk_widget_show(GTK_WIDGET(header_bar)); + gtk_header_bar_set_title(header_bar, "example"); + gtk_header_bar_set_show_close_button(header_bar, TRUE); + gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); + } else { + gtk_window_set_title(window, "example"); + } + + gtk_window_set_default_size(window, 1280, 720); + gtk_widget_show(GTK_WIDGET(window)); + + g_autoptr(FlDartProject) project = fl_dart_project_new(); + fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments); + + FlView* view = fl_view_new(project); + gtk_widget_show(GTK_WIDGET(view)); + gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); + + fl_register_plugins(FL_PLUGIN_REGISTRY(view)); + + gtk_widget_grab_focus(GTK_WIDGET(view)); +} + +// Implements GApplication::local_command_line. +static gboolean my_application_local_command_line(GApplication* application, gchar*** arguments, int* exit_status) { + MyApplication* self = MY_APPLICATION(application); + // Strip out the first argument as it is the binary name. + self->dart_entrypoint_arguments = g_strdupv(*arguments + 1); + + g_autoptr(GError) error = nullptr; + if (!g_application_register(application, nullptr, &error)) { + g_warning("Failed to register: %s", error->message); + *exit_status = 1; + return TRUE; + } + + g_application_activate(application); + *exit_status = 0; + + return TRUE; +} + +// Implements GObject::dispose. +static void my_application_dispose(GObject* object) { + MyApplication* self = MY_APPLICATION(object); + g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev); + G_OBJECT_CLASS(my_application_parent_class)->dispose(object); +} + +static void my_application_class_init(MyApplicationClass* klass) { + G_APPLICATION_CLASS(klass)->activate = my_application_activate; + G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line; + G_OBJECT_CLASS(klass)->dispose = my_application_dispose; +} + +static void my_application_init(MyApplication* self) {} + +MyApplication* my_application_new() { + return MY_APPLICATION(g_object_new(my_application_get_type(), + "application-id", APPLICATION_ID, + "flags", G_APPLICATION_NON_UNIQUE, + nullptr)); +} diff --git a/example/linux/my_application.h b/example/linux/my_application.h index 8f20fb55..72271d5e 100644 --- a/example/linux/my_application.h +++ b/example/linux/my_application.h @@ -1,18 +1,18 @@ -#ifndef FLUTTER_MY_APPLICATION_H_ -#define FLUTTER_MY_APPLICATION_H_ - -#include - -G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION, - GtkApplication) - -/** - * my_application_new: - * - * Creates a new Flutter-based application. - * - * Returns: a new #MyApplication. - */ -MyApplication* my_application_new(); - -#endif // FLUTTER_MY_APPLICATION_H_ +#ifndef FLUTTER_MY_APPLICATION_H_ +#define FLUTTER_MY_APPLICATION_H_ + +#include + +G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION, + GtkApplication) + +/** + * my_application_new: + * + * Creates a new Flutter-based application. + * + * Returns: a new #MyApplication. + */ +MyApplication* my_application_new(); + +#endif // FLUTTER_MY_APPLICATION_H_ diff --git a/example/macos/.gitignore b/example/macos/.gitignore index e72996ef..746adbb6 100644 --- a/example/macos/.gitignore +++ b/example/macos/.gitignore @@ -1,7 +1,7 @@ -# Flutter-related -**/Flutter/ephemeral/ -**/Pods/ - -# Xcode-related -**/xcuserdata/ -Podfile.lock \ No newline at end of file +# Flutter-related +**/Flutter/ephemeral/ +**/Pods/ + +# Xcode-related +**/dgph +**/xcuserdata/ diff --git a/example/macos/Flutter/Flutter-Debug.xcconfig b/example/macos/Flutter/Flutter-Debug.xcconfig index df4c964c..4b81f9b2 100644 --- a/example/macos/Flutter/Flutter-Debug.xcconfig +++ b/example/macos/Flutter/Flutter-Debug.xcconfig @@ -1,2 +1,2 @@ #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" -#include "ephemeral/Flutter-Generated.xcconfig" +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/example/macos/Flutter/Flutter-Release.xcconfig b/example/macos/Flutter/Flutter-Release.xcconfig index e79501e2..5caa9d15 100644 --- a/example/macos/Flutter/Flutter-Release.xcconfig +++ b/example/macos/Flutter/Flutter-Release.xcconfig @@ -1,2 +1,2 @@ #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" -#include "ephemeral/Flutter-Generated.xcconfig" +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/example/macos/Flutter/GeneratedPluginRegistrant.swift b/example/macos/Flutter/GeneratedPluginRegistrant.swift index 9245196c..6b3c06b5 100644 --- a/example/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/example/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,20 +5,26 @@ import FlutterMacOS import Foundation +import desktop_drop import device_info_plus import file_selector_macos import gal import pasteboard import path_provider_foundation +import share_plus +import sqflite import url_launcher_macos import video_player_avfoundation func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + DesktopDropPlugin.register(with: registry.registrar(forPlugin: "DesktopDropPlugin")) DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) GalPlugin.register(with: registry.registrar(forPlugin: "GalPlugin")) PasteboardPlugin.register(with: registry.registrar(forPlugin: "PasteboardPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) + SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin")) + SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) FVPVideoPlayerPlugin.register(with: registry.registrar(forPlugin: "FVPVideoPlayerPlugin")) } diff --git a/example/macos/Podfile b/example/macos/Podfile index 0c76ccf5..dbccf89c 100644 --- a/example/macos/Podfile +++ b/example/macos/Podfile @@ -1,4 +1,4 @@ -platform :osx, '12.0' +platform :osx, '11.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' @@ -31,6 +31,9 @@ target 'Runner' do use_modular_headers! flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + end end post_install do |installer| diff --git a/example/macos/Podfile.lock b/example/macos/Podfile.lock new file mode 100644 index 00000000..0df3e6fc --- /dev/null +++ b/example/macos/Podfile.lock @@ -0,0 +1,88 @@ +PODS: + - desktop_drop (0.0.1): + - FlutterMacOS + - device_info_plus (0.0.1): + - FlutterMacOS + - file_selector_macos (0.0.1): + - FlutterMacOS + - FlutterMacOS (1.0.0) + - FMDB (2.7.5): + - FMDB/standard (= 2.7.5) + - FMDB/standard (2.7.5) + - gal (1.0.0): + - Flutter + - FlutterMacOS + - pasteboard (0.0.1): + - FlutterMacOS + - path_provider_foundation (0.0.1): + - Flutter + - FlutterMacOS + - share_plus (0.0.1): + - FlutterMacOS + - sqflite (0.0.2): + - FlutterMacOS + - FMDB (>= 2.7.5) + - url_launcher_macos (0.0.1): + - FlutterMacOS + - video_player_avfoundation (0.0.1): + - Flutter + - FlutterMacOS + +DEPENDENCIES: + - desktop_drop (from `Flutter/ephemeral/.symlinks/plugins/desktop_drop/macos`) + - device_info_plus (from `Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos`) + - file_selector_macos (from `Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos`) + - FlutterMacOS (from `Flutter/ephemeral`) + - gal (from `Flutter/ephemeral/.symlinks/plugins/gal/darwin`) + - pasteboard (from `Flutter/ephemeral/.symlinks/plugins/pasteboard/macos`) + - path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`) + - share_plus (from `Flutter/ephemeral/.symlinks/plugins/share_plus/macos`) + - sqflite (from `Flutter/ephemeral/.symlinks/plugins/sqflite/macos`) + - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) + - video_player_avfoundation (from `Flutter/ephemeral/.symlinks/plugins/video_player_avfoundation/darwin`) + +SPEC REPOS: + trunk: + - FMDB + +EXTERNAL SOURCES: + desktop_drop: + :path: Flutter/ephemeral/.symlinks/plugins/desktop_drop/macos + device_info_plus: + :path: Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos + file_selector_macos: + :path: Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos + FlutterMacOS: + :path: Flutter/ephemeral + gal: + :path: Flutter/ephemeral/.symlinks/plugins/gal/darwin + pasteboard: + :path: Flutter/ephemeral/.symlinks/plugins/pasteboard/macos + path_provider_foundation: + :path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin + share_plus: + :path: Flutter/ephemeral/.symlinks/plugins/share_plus/macos + sqflite: + :path: Flutter/ephemeral/.symlinks/plugins/sqflite/macos + url_launcher_macos: + :path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos + video_player_avfoundation: + :path: Flutter/ephemeral/.symlinks/plugins/video_player_avfoundation/darwin + +SPEC CHECKSUMS: + desktop_drop: 69eeff437544aa619c8db7f4481b3a65f7696898 + device_info_plus: 5401765fde0b8d062a2f8eb65510fb17e77cf07f + file_selector_macos: 468fb6b81fac7c0e88d71317f3eec34c3b008ff9 + FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 + FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a + gal: 61e868295d28fe67ffa297fae6dacebf56fd53e1 + pasteboard: 9b69dba6fedbb04866be632205d532fe2f6b1d99 + path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943 + share_plus: 76dd39142738f7a68dd57b05093b5e8193f220f7 + sqflite: a5789cceda41d54d23f31d6de539d65bb14100ea + url_launcher_macos: d2691c7dd33ed713bf3544850a623080ec693d95 + video_player_avfoundation: 8563f13d8fc8b2c29dc2d09e60b660e4e8128837 + +PODFILE CHECKSUM: c2e95c8c0fe03c5c57e438583cae4cc732296009 + +COCOAPODS: 1.14.2 diff --git a/example/macos/Runner.xcodeproj/project.pbxproj b/example/macos/Runner.xcodeproj/project.pbxproj index 29557646..9d12e3f3 100644 --- a/example/macos/Runner.xcodeproj/project.pbxproj +++ b/example/macos/Runner.xcodeproj/project.pbxproj @@ -21,7 +21,9 @@ /* End PBXAggregateTarget section */ /* Begin PBXBuildFile section */ - 07D884DE6AB8033C3F60B238 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 48A88899E2BC5FD7AFD2B040 /* Pods_Runner.framework */; }; + 0819A4118119F0FB90CC792A /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DF9630B171C82D9F157D8A7A /* Pods_RunnerTests.framework */; }; + 0ECAC09CE6CB433383FE46BA /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F25EE7A42A9A7B340553AFD5 /* Pods_Runner.framework */; }; + 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C80D7294CF71000263BE5 /* RunnerTests.swift */; }; 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; @@ -30,6 +32,13 @@ /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ + 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC10EC2044A3C60003C045; + remoteInfo = Runner; + }; 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 33CC10E52044A3C60003C045 /* Project object */; @@ -53,10 +62,14 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ - 17BD0969A552CE47C17FC221 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + 08857C82C866FAA72A6112E1 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; + 1AE7B8BE0097CC9BD2CEB71C /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + 213343C812B541CBD55EC002 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + 331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; - 33CC10ED2044A3C60003C045 /* app.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = app.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10ED2044A3C60003C045 /* example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = example.app; sourceTree = BUILT_PRODUCTS_DIR; }; 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; @@ -68,25 +81,43 @@ 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; - 3DB40E993F068140F6DEEA8F /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; - 48A88899E2BC5FD7AFD2B040 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 676737A1C184536E1D9D90A1 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + 382159709DCCA9047A6B60BF /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; + AD4F0BE8B3C0868B8BCD1802 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; + B6A180C3D11679D4FEC36007 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + DF9630B171C82D9F157D8A7A /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + F25EE7A42A9A7B340553AFD5 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ + 331C80D2294CF70F00263BE5 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 0819A4118119F0FB90CC792A /* Pods_RunnerTests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 33CC10EA2044A3C60003C045 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 07D884DE6AB8033C3F60B238 /* Pods_Runner.framework in Frameworks */, + 0ECAC09CE6CB433383FE46BA /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 331C80D6294CF71000263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C80D7294CF71000263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; 33BA886A226E78AF003329D5 /* Configs */ = { isa = PBXGroup; children = ( @@ -103,16 +134,18 @@ children = ( 33FAB671232836740065AC1E /* Runner */, 33CEB47122A05771004F2AC0 /* Flutter */, + 331C80D6294CF71000263BE5 /* RunnerTests */, 33CC10EE2044A3C60003C045 /* Products */, - E0CAA5D4D3AFCAEB94FF2464 /* Pods */, - C2525C9EE4B6956CB985C5A2 /* Frameworks */, + 50A36B439D603B27B2A10103 /* Pods */, + 6BD6849B62426A4BAFC71567 /* Frameworks */, ); sourceTree = ""; }; 33CC10EE2044A3C60003C045 /* Products */ = { isa = PBXGroup; children = ( - 33CC10ED2044A3C60003C045 /* app.app */, + 33CC10ED2044A3C60003C045 /* example.app */, + 331C80D5294CF71000263BE5 /* RunnerTests.xctest */, ); name = Products; sourceTree = ""; @@ -152,39 +185,62 @@ path = Runner; sourceTree = ""; }; - C2525C9EE4B6956CB985C5A2 /* Frameworks */ = { + 50A36B439D603B27B2A10103 /* Pods */ = { isa = PBXGroup; children = ( - 48A88899E2BC5FD7AFD2B040 /* Pods_Runner.framework */, + 213343C812B541CBD55EC002 /* Pods-Runner.debug.xcconfig */, + B6A180C3D11679D4FEC36007 /* Pods-Runner.release.xcconfig */, + 1AE7B8BE0097CC9BD2CEB71C /* Pods-Runner.profile.xcconfig */, + AD4F0BE8B3C0868B8BCD1802 /* Pods-RunnerTests.debug.xcconfig */, + 08857C82C866FAA72A6112E1 /* Pods-RunnerTests.release.xcconfig */, + 382159709DCCA9047A6B60BF /* Pods-RunnerTests.profile.xcconfig */, ); - name = Frameworks; + name = Pods; + path = Pods; sourceTree = ""; }; - E0CAA5D4D3AFCAEB94FF2464 /* Pods */ = { + 6BD6849B62426A4BAFC71567 /* Frameworks */ = { isa = PBXGroup; children = ( - 3DB40E993F068140F6DEEA8F /* Pods-Runner.debug.xcconfig */, - 676737A1C184536E1D9D90A1 /* Pods-Runner.release.xcconfig */, - 17BD0969A552CE47C17FC221 /* Pods-Runner.profile.xcconfig */, + F25EE7A42A9A7B340553AFD5 /* Pods_Runner.framework */, + DF9630B171C82D9F157D8A7A /* Pods_RunnerTests.framework */, ); - name = Pods; - path = Pods; + name = Frameworks; sourceTree = ""; }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ + 331C80D4294CF70F00263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + B106B712E930130681A09692 /* [CP] Check Pods Manifest.lock */, + 331C80D1294CF70F00263BE5 /* Sources */, + 331C80D2294CF70F00263BE5 /* Frameworks */, + 331C80D3294CF70F00263BE5 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 331C80DA294CF71000263BE5 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 331C80D5294CF71000263BE5 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; 33CC10EC2044A3C60003C045 /* Runner */ = { isa = PBXNativeTarget; buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( - 8E0B73589C7156B8D6C458C1 /* [CP] Check Pods Manifest.lock */, + 1C8E506F11B9603AA202D203 /* [CP] Check Pods Manifest.lock */, 33CC10E92044A3C60003C045 /* Sources */, 33CC10EA2044A3C60003C045 /* Frameworks */, 33CC10EB2044A3C60003C045 /* Resources */, 33CC110E2044A8840003C045 /* Bundle Framework */, 3399D490228B24CF009A79C7 /* ShellScript */, - 64BF2FA23C00365B5E3F66C0 /* [CP] Embed Pods Frameworks */, + CCF759434F90A85A86F6BD32 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -193,7 +249,7 @@ ); name = Runner; productName = Runner; - productReference = 33CC10ED2044A3C60003C045 /* app.app */; + productReference = 33CC10ED2044A3C60003C045 /* example.app */; productType = "com.apple.product-type.application"; }; /* End PBXNativeTarget section */ @@ -206,6 +262,10 @@ LastUpgradeCheck = 1430; ORGANIZATIONNAME = ""; TargetAttributes = { + 331C80D4294CF70F00263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 33CC10EC2044A3C60003C045; + }; 33CC10EC2044A3C60003C045 = { CreatedOnToolsVersion = 9.2; LastSwiftMigration = 1100; @@ -236,12 +296,20 @@ projectRoot = ""; targets = ( 33CC10EC2044A3C60003C045 /* Runner */, + 331C80D4294CF70F00263BE5 /* RunnerTests */, 33CC111A2044C6BA0003C045 /* Flutter Assemble */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ + 331C80D3294CF70F00263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; 33CC10EB2044A3C60003C045 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -254,6 +322,28 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ + 1C8E506F11B9603AA202D203 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; 3399D490228B24CF009A79C7 /* ShellScript */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; @@ -292,48 +382,56 @@ shellPath = /bin/sh; shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; }; - 64BF2FA23C00365B5E3F66C0 /* [CP] Embed Pods Frameworks */ = { + B106B712E930130681A09692 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); - name = "[CP] Embed Pods Frameworks"; + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; - 8E0B73589C7156B8D6C458C1 /* [CP] Check Pods Manifest.lock */ = { + CCF759434F90A85A86F6BD32 /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; + name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( - ); - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ + 331C80D1294CF70F00263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 33CC10E92044A3C60003C045 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -347,6 +445,11 @@ /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ + 331C80DA294CF71000263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC10EC2044A3C60003C045 /* Runner */; + targetProxy = 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */; + }; 33CC11202044C79F0003C045 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */; @@ -367,6 +470,51 @@ /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ + 331C80DB294CF71000263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = AD4F0BE8B3C0868B8BCD1802 /* Pods-RunnerTests.debug.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.example.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/example.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/example"; + }; + name = Debug; + }; + 331C80DC294CF71000263BE5 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 08857C82C866FAA72A6112E1 /* Pods-RunnerTests.release.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.example.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/example.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/example"; + }; + name = Release; + }; + 331C80DD294CF71000263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 382159709DCCA9047A6B60BF /* Pods-RunnerTests.profile.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.example.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/example.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/example"; + }; + name = Profile; + }; 338D0CE9231458BD00FA5F75 /* Profile */ = { isa = XCBuildConfiguration; baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; @@ -405,7 +553,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = "$(RECOMMENDED_MACOSX_DEPLOYMENT_TARGET)"; + MACOSX_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; @@ -427,7 +575,7 @@ "$(inherited)", "@executable_path/../Frameworks", ); - MACOSX_DEPLOYMENT_TARGET = 12.0; + MACOSX_DEPLOYMENT_TARGET = 11.0; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_VERSION = 5.0; }; @@ -485,7 +633,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = "$(RECOMMENDED_MACOSX_DEPLOYMENT_TARGET)"; + MACOSX_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = macosx; @@ -532,7 +680,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = "$(RECOMMENDED_MACOSX_DEPLOYMENT_TARGET)"; + MACOSX_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; @@ -554,7 +702,7 @@ "$(inherited)", "@executable_path/../Frameworks", ); - MACOSX_DEPLOYMENT_TARGET = 12.0; + MACOSX_DEPLOYMENT_TARGET = 11.0; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; @@ -575,7 +723,7 @@ "$(inherited)", "@executable_path/../Frameworks", ); - MACOSX_DEPLOYMENT_TARGET = 12.0; + MACOSX_DEPLOYMENT_TARGET = 11.0; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_VERSION = 5.0; }; @@ -600,6 +748,16 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ + 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 331C80DB294CF71000263BE5 /* Debug */, + 331C80DC294CF71000263BE5 /* Release */, + 331C80DD294CF71000263BE5 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist index fc6bf807..18d98100 100644 --- a/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ b/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -1,8 +1,8 @@ - - - - - IDEDidComputeMac32BitWarning - - - + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 8cbaa660..397f3d33 100644 --- a/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -15,7 +15,7 @@ @@ -31,13 +31,24 @@ - - + + + + + + - - diff --git a/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist index fc6bf807..18d98100 100644 --- a/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ b/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -1,8 +1,8 @@ - - - - - IDEDidComputeMac32BitWarning - - - + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/example/macos/Runner/AppDelegate.swift b/example/macos/Runner/AppDelegate.swift index 553a135b..d53ef643 100644 --- a/example/macos/Runner/AppDelegate.swift +++ b/example/macos/Runner/AppDelegate.swift @@ -1,9 +1,9 @@ -import Cocoa -import FlutterMacOS - -@NSApplicationMain -class AppDelegate: FlutterAppDelegate { - override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { - return true - } -} +import Cocoa +import FlutterMacOS + +@NSApplicationMain +class AppDelegate: FlutterAppDelegate { + override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { + return true + } +} diff --git a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json index 8d4e7cb8..a2ec33f1 100644 --- a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -1,68 +1,68 @@ -{ - "images" : [ - { - "size" : "16x16", - "idiom" : "mac", - "filename" : "app_icon_16.png", - "scale" : "1x" - }, - { - "size" : "16x16", - "idiom" : "mac", - "filename" : "app_icon_32.png", - "scale" : "2x" - }, - { - "size" : "32x32", - "idiom" : "mac", - "filename" : "app_icon_32.png", - "scale" : "1x" - }, - { - "size" : "32x32", - "idiom" : "mac", - "filename" : "app_icon_64.png", - "scale" : "2x" - }, - { - "size" : "128x128", - "idiom" : "mac", - "filename" : "app_icon_128.png", - "scale" : "1x" - }, - { - "size" : "128x128", - "idiom" : "mac", - "filename" : "app_icon_256.png", - "scale" : "2x" - }, - { - "size" : "256x256", - "idiom" : "mac", - "filename" : "app_icon_256.png", - "scale" : "1x" - }, - { - "size" : "256x256", - "idiom" : "mac", - "filename" : "app_icon_512.png", - "scale" : "2x" - }, - { - "size" : "512x512", - "idiom" : "mac", - "filename" : "app_icon_512.png", - "scale" : "1x" - }, - { - "size" : "512x512", - "idiom" : "mac", - "filename" : "app_icon_1024.png", - "scale" : "2x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} +{ + "images" : [ + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_16.png", + "scale" : "1x" + }, + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "2x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "1x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_64.png", + "scale" : "2x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_128.png", + "scale" : "1x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "2x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "1x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "2x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "1x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_1024.png", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png index 3c4935a7..82b6f9d9 100644 Binary files a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png and b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png differ diff --git a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png index ed4cc164..13b35eba 100644 Binary files a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png and b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png differ diff --git a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png index 483be613..0a3f5fa4 100644 Binary files a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png and b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png differ diff --git a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png index bcbf36df..bdb57226 100644 Binary files a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png and b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png differ diff --git a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png index 9c0a6528..f083318e 100644 Binary files a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png and b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png differ diff --git a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png index e71a7261..326c0e72 100644 Binary files a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png and b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png differ diff --git a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png index 8a31fe2d..2f1632cf 100644 Binary files a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png and b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png differ diff --git a/example/macos/Runner/Base.lproj/MainMenu.xib b/example/macos/Runner/Base.lproj/MainMenu.xib index 030024dc..80e867a4 100644 --- a/example/macos/Runner/Base.lproj/MainMenu.xib +++ b/example/macos/Runner/Base.lproj/MainMenu.xib @@ -1,339 +1,343 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/example/macos/Runner/Configs/AppInfo.xcconfig b/example/macos/Runner/Configs/AppInfo.xcconfig index 38c93ba6..dda192bc 100644 --- a/example/macos/Runner/Configs/AppInfo.xcconfig +++ b/example/macos/Runner/Configs/AppInfo.xcconfig @@ -1,14 +1,14 @@ -// Application-level settings for the Runner target. -// -// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the -// future. If not, the values below would default to using the project name when this becomes a -// 'flutter create' template. - -// The application's name. By default this is also the title of the Flutter window. -PRODUCT_NAME = app - -// The application's bundle identifier -PRODUCT_BUNDLE_IDENTIFIER = com.example.app - -// The copyright displayed in application information -PRODUCT_COPYRIGHT = Copyright © 2021 com.example. All rights reserved. +// Application-level settings for the Runner target. +// +// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the +// future. If not, the values below would default to using the project name when this becomes a +// 'flutter create' template. + +// The application's name. By default this is also the title of the Flutter window. +PRODUCT_NAME = example + +// The application's bundle identifier +PRODUCT_BUNDLE_IDENTIFIER = com.example.example + +// The copyright displayed in application information +PRODUCT_COPYRIGHT = Copyright © 2023 com.example. All rights reserved. diff --git a/example/macos/Runner/Configs/Debug.xcconfig b/example/macos/Runner/Configs/Debug.xcconfig index b3988237..36b0fd94 100644 --- a/example/macos/Runner/Configs/Debug.xcconfig +++ b/example/macos/Runner/Configs/Debug.xcconfig @@ -1,2 +1,2 @@ -#include "../../Flutter/Flutter-Debug.xcconfig" -#include "Warnings.xcconfig" +#include "../../Flutter/Flutter-Debug.xcconfig" +#include "Warnings.xcconfig" diff --git a/example/macos/Runner/Configs/Release.xcconfig b/example/macos/Runner/Configs/Release.xcconfig index d93e5dc4..dff4f495 100644 --- a/example/macos/Runner/Configs/Release.xcconfig +++ b/example/macos/Runner/Configs/Release.xcconfig @@ -1,2 +1,2 @@ -#include "../../Flutter/Flutter-Release.xcconfig" -#include "Warnings.xcconfig" +#include "../../Flutter/Flutter-Release.xcconfig" +#include "Warnings.xcconfig" diff --git a/example/macos/Runner/Configs/Warnings.xcconfig b/example/macos/Runner/Configs/Warnings.xcconfig index fb4d7d3f..42bcbf47 100644 --- a/example/macos/Runner/Configs/Warnings.xcconfig +++ b/example/macos/Runner/Configs/Warnings.xcconfig @@ -1,13 +1,13 @@ -WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings -GCC_WARN_UNDECLARED_SELECTOR = YES -CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES -CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE -CLANG_WARN__DUPLICATE_METHOD_MATCH = YES -CLANG_WARN_PRAGMA_PACK = YES -CLANG_WARN_STRICT_PROTOTYPES = YES -CLANG_WARN_COMMA = YES -GCC_WARN_STRICT_SELECTOR_MATCH = YES -CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES -CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES -GCC_WARN_SHADOW = YES -CLANG_WARN_UNREACHABLE_CODE = YES +WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings +GCC_WARN_UNDECLARED_SELECTOR = YES +CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES +CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE +CLANG_WARN__DUPLICATE_METHOD_MATCH = YES +CLANG_WARN_PRAGMA_PACK = YES +CLANG_WARN_STRICT_PROTOTYPES = YES +CLANG_WARN_COMMA = YES +GCC_WARN_STRICT_SELECTOR_MATCH = YES +CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES +CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES +GCC_WARN_SHADOW = YES +CLANG_WARN_UNREACHABLE_CODE = YES diff --git a/example/macos/Runner/DebugProfile.entitlements b/example/macos/Runner/DebugProfile.entitlements index d6c8ee0c..b3feca3e 100644 --- a/example/macos/Runner/DebugProfile.entitlements +++ b/example/macos/Runner/DebugProfile.entitlements @@ -1,14 +1,18 @@ - - - - - com.apple.security.app-sandbox - - com.apple.security.cs.allow-jit - - com.apple.security.network.server - - com.apple.security.network.client - - - + + + + + com.apple.security.app-sandbox + + com.apple.security.cs.allow-jit + + com.apple.security.network.server + + com.apple.security.network.client + + com.apple.security.files.user-selected.read-only + + + + diff --git a/example/macos/Runner/Info.plist b/example/macos/Runner/Info.plist index 3733c1a8..0eae602e 100644 --- a/example/macos/Runner/Info.plist +++ b/example/macos/Runner/Info.plist @@ -1,32 +1,36 @@ - - - - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIconFile - - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - APPL - CFBundleShortVersionString - $(FLUTTER_BUILD_NAME) - CFBundleVersion - $(FLUTTER_BUILD_NUMBER) - LSMinimumSystemVersion - $(MACOSX_DEPLOYMENT_TARGET) - NSHumanReadableCopyright - $(PRODUCT_COPYRIGHT) - NSMainNibFile - MainMenu - NSPrincipalClass - NSApplication - - + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIconFile + + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSMinimumSystemVersion + $(MACOSX_DEPLOYMENT_TARGET) + NSHumanReadableCopyright + $(PRODUCT_COPYRIGHT) + NSMainNibFile + MainMenu + NSPrincipalClass + NSApplication + NSPhotoLibraryUsageDescription + We need permission to the photo library in order for inserting images in the text editor + NSPhotoLibraryAddUsageDescription + We need this permission for saving the images in the editor + + diff --git a/example/macos/Runner/MainFlutterWindow.swift b/example/macos/Runner/MainFlutterWindow.swift index 4cb95dc9..3cc05eb2 100644 --- a/example/macos/Runner/MainFlutterWindow.swift +++ b/example/macos/Runner/MainFlutterWindow.swift @@ -1,15 +1,15 @@ -import Cocoa -import FlutterMacOS - -class MainFlutterWindow: NSWindow { - override func awakeFromNib() { - let flutterViewController = FlutterViewController.init() - let windowFrame = self.frame - self.contentViewController = flutterViewController - self.setFrame(windowFrame, display: true) - - RegisterGeneratedPlugins(registry: flutterViewController) - - super.awakeFromNib() - } -} +import Cocoa +import FlutterMacOS + +class MainFlutterWindow: NSWindow { + override func awakeFromNib() { + let flutterViewController = FlutterViewController() + let windowFrame = self.frame + self.contentViewController = flutterViewController + self.setFrame(windowFrame, display: true) + + RegisterGeneratedPlugins(registry: flutterViewController) + + super.awakeFromNib() + } +} diff --git a/example/macos/Runner/Release.entitlements b/example/macos/Runner/Release.entitlements index 04336df3..319280f5 100644 --- a/example/macos/Runner/Release.entitlements +++ b/example/macos/Runner/Release.entitlements @@ -1,8 +1,14 @@ - - - - - com.apple.security.app-sandbox - - - + + + + + com.apple.security.app-sandbox + + com.apple.security.network.client + + com.apple.security.files.user-selected.read-only + + + + diff --git a/example/macos/RunnerTests/RunnerTests.swift b/example/macos/RunnerTests/RunnerTests.swift new file mode 100644 index 00000000..5418c9f5 --- /dev/null +++ b/example/macos/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import FlutterMacOS +import Cocoa +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. + } + +} diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 4053c2c7..dc0df887 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -1,39 +1,77 @@ -name: app -description: demo app +name: example +description: A demo for the Flutter Quill publish_to: 'none' version: 1.0.0+1 environment: - sdk: '>=2.12.0 <3.0.0' - + sdk: '>=3.1.5 <4.0.0' dependencies: flutter: sdk: flutter - universal_html: ^2.2.4 - + flutter_localizations: + sdk: flutter cupertino_icons: ^1.0.6 + + # Flutter Quill Packages + flutter_quill: ^8.5.5 + flutter_quill_extensions: ^0.6.10 + flutter_quill_test: ^0.0.5 + quill_html_converter: ^0.0.1-experimental.1 + + # Normal Packages + path: ^1.8.3 + equatable: ^2.0.5 + cross_file: ^0.3.3+6 + cached_network_image: ^3.3.0 + + # Bloc libraries + bloc: ^8.1.2 + flutter_bloc: ^8.1.3 + hydrated_bloc: ^9.1.2 + + # Freezed + freezed_annotation: ^2.4.1 + + # Json + json_annotation: ^4.8.1 + + # Plugins + image_cropper: ^5.0.0 path_provider: ^2.1.1 - filesystem_picker: ^4.0.0 - file_picker: ^6.0.0 - flutter_quill: - path: ../ - flutter_quill_extensions: - path: ../flutter_quill_extensions + # For drag and drop feature + desktop_drop: ^0.4.4 + # For picking quill document files + file_picker: ^6.1.1 + # For sharing text + share_plus: ^7.2.1 dependency_overrides: flutter_quill: path: ../ + flutter_quill_extensions: + path: ../flutter_quill_extensions + flutter_quill_test: + path: ../flutter_quill_test + quill_html_converter: + path: ../packages/quill_html_converter dev_dependencies: flutter_test: sdk: flutter + flutter_lints: ^3.0.1 + build_runner: ^2.4.6 + flutter_gen_runner: ^5.3.2 + # Freezed + freezed: ^2.4.5 + # Json + json_serializable: ^6.7.1 flutter: - uses-material-design: true assets: - assets/ - + - assets/images/ + fonts: - family: monospace fonts: @@ -61,4 +99,11 @@ flutter: - asset: assets/fonts/RobotoMono-Regular.ttf - family: SF-UI-Display fonts: - - asset: assets/fonts/SF-Pro-Display-Regular.otf \ No newline at end of file + - asset: assets/fonts/SF-Pro-Display-Regular.otf + +flutter_gen: + # integrations: + # flutter_svg: true + # flare_flutter: true + # rive: true + # lottie: true diff --git a/example/test/widget_test.dart b/example/test/widget_test.dart index ca3ecf17..ab73b3a2 100644 --- a/example/test/widget_test.dart +++ b/example/test/widget_test.dart @@ -1,29 +1 @@ -// This is a basic Flutter widget test. -// -// To perform an interaction with a widget in your test, use the WidgetTester -// utility that Flutter provides. For example, you can send tap and scroll -// gestures. You can also use WidgetTester to find child widgets in the widget -// tree, read text, and verify that the values of widget properties are correct. - -import 'package:app/main.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; - -void main() { - testWidgets('Counter increments smoke test', (tester) async { - // Build our app and trigger a frame. - await tester.pumpWidget(MyApp()); - - // Verify that our counter starts at 0. - expect(find.text('0'), findsOneWidget); - expect(find.text('1'), findsNothing); - - // Tap the '+' icon and trigger a frame. - await tester.tap(find.byIcon(Icons.add)); - await tester.pump(); - - // Verify that our counter has incremented. - expect(find.text('0'), findsNothing); - expect(find.text('1'), findsOneWidget); - }); -} +void main() {} diff --git a/example/web/icons/Icon-maskable-192.png b/example/web/icons/Icon-maskable-192.png new file mode 100644 index 00000000..eb9b4d76 Binary files /dev/null and b/example/web/icons/Icon-maskable-192.png differ diff --git a/example/web/icons/Icon-maskable-512.png b/example/web/icons/Icon-maskable-512.png new file mode 100644 index 00000000..d69c5669 Binary files /dev/null and b/example/web/icons/Icon-maskable-512.png differ diff --git a/example/web/index.html b/example/web/index.html index 887a139d..f998cef3 100644 --- a/example/web/index.html +++ b/example/web/index.html @@ -8,10 +8,13 @@ The path provided below has to start and end with a slash "/" in order for it to work correctly. - Fore more details: + For more details: * https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base + + This is a placeholder for base href that will be replaced by the value of + the `--base-href` argument provided to `flutter build`. --> - + @@ -20,26 +23,42 @@ - + - app + example + + + + + + + + + - - diff --git a/example/web/manifest.json b/example/web/manifest.json index c3c31e3b..096edf8f 100644 --- a/example/web/manifest.json +++ b/example/web/manifest.json @@ -1,6 +1,6 @@ { - "name": "app", - "short_name": "app", + "name": "example", + "short_name": "example", "start_url": ".", "display": "standalone", "background_color": "#0175C2", @@ -18,6 +18,18 @@ "src": "icons/Icon-512.png", "sizes": "512x512", "type": "image/png" + }, + { + "src": "icons/Icon-maskable-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "icons/Icon-maskable-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" } ] } diff --git a/example/windows/.gitignore b/example/windows/.gitignore index ec4098aa..d492d0d9 100644 --- a/example/windows/.gitignore +++ b/example/windows/.gitignore @@ -1,17 +1,17 @@ -flutter/ephemeral/ - -# Visual Studio user-specific files. -*.suo -*.user -*.userosscache -*.sln.docstates - -# Visual Studio build-related files. -x64/ -x86/ - -# Visual Studio cache files -# files ending in .cache can be ignored -*.[Cc]ache -# but keep track of directories ending in .cache -!*.[Cc]ache/ +flutter/ephemeral/ + +# Visual Studio user-specific files. +*.suo +*.user +*.userosscache +*.sln.docstates + +# Visual Studio build-related files. +x64/ +x86/ + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ diff --git a/example/windows/CMakeLists.txt b/example/windows/CMakeLists.txt index c4363c4c..c09389c5 100644 --- a/example/windows/CMakeLists.txt +++ b/example/windows/CMakeLists.txt @@ -1,95 +1,102 @@ -cmake_minimum_required(VERSION 3.15) -project(app LANGUAGES CXX) - -set(BINARY_NAME "app") - -cmake_policy(SET CMP0063 NEW) - -set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") - -# Configure build options. -get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) -if(IS_MULTICONFIG) - set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" - CACHE STRING "" FORCE) -else() - if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) - set(CMAKE_BUILD_TYPE "Debug" CACHE - STRING "Flutter build mode" FORCE) - set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS - "Debug" "Profile" "Release") - endif() -endif() - -set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") -set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") -set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") -set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") - -# Use Unicode for all projects. -add_definitions(-DUNICODE -D_UNICODE) - -# Compilation settings that should be applied to most targets. -function(APPLY_STANDARD_SETTINGS TARGET) - target_compile_features(${TARGET} PUBLIC cxx_std_17) - target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") - target_compile_options(${TARGET} PRIVATE /EHsc) - target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") - target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") -endfunction() - -set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") - -# Flutter library and tool build rules. -add_subdirectory(${FLUTTER_MANAGED_DIR}) - -# Application build -add_subdirectory("runner") - -# Generated plugin build rules, which manage building the plugins and adding -# them to the application. -include(flutter/generated_plugins.cmake) - - -# === Installation === -# Support files are copied into place next to the executable, so that it can -# run in place. This is done instead of making a separate bundle (as on Linux) -# so that building and running from within Visual Studio will work. -set(BUILD_BUNDLE_DIR "$") -# Make the "install" step default, as it's required to run. -set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) -if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) - set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) -endif() - -set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") -set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") - -install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" - COMPONENT Runtime) - -install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" - COMPONENT Runtime) - -install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" - COMPONENT Runtime) - -if(PLUGIN_BUNDLED_LIBRARIES) - install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" - DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" - COMPONENT Runtime) -endif() - -# Fully re-copy the assets directory on each build to avoid having stale files -# from a previous install. -set(FLUTTER_ASSET_DIR_NAME "flutter_assets") -install(CODE " - file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") - " COMPONENT Runtime) -install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" - DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) - -# Install the AOT library on non-Debug builds only. -install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" - CONFIGURATIONS Profile;Release - COMPONENT Runtime) +# Project-level configuration. +cmake_minimum_required(VERSION 3.14) +project(example LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "example") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(VERSION 3.14...3.25) + +# Define build configuration option. +get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) +if(IS_MULTICONFIG) + set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" + CACHE STRING "" FORCE) +else() + if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") + endif() +endif() +# Define settings for the Profile build mode. +set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") +set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") +set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") +set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") + +# Use Unicode for all projects. +add_definitions(-DUNICODE -D_UNICODE) + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_17) + target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") + target_compile_options(${TARGET} PRIVATE /EHsc) + target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") + target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# Application build; see runner/CMakeLists.txt. +add_subdirectory("runner") + + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# Support files are copied into place next to the executable, so that it can +# run in place. This is done instead of making a separate bundle (as on Linux) +# so that building and running from within Visual Studio will work. +set(BUILD_BUNDLE_DIR "$") +# Make the "install" step default, as it's required to run. +set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +if(PLUGIN_BUNDLED_LIBRARIES) + install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + CONFIGURATIONS Profile;Release + COMPONENT Runtime) diff --git a/example/windows/flutter/CMakeLists.txt b/example/windows/flutter/CMakeLists.txt index c10f4f62..930d2071 100644 --- a/example/windows/flutter/CMakeLists.txt +++ b/example/windows/flutter/CMakeLists.txt @@ -1,103 +1,104 @@ -cmake_minimum_required(VERSION 3.15) - -set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") - -# Configuration provided via flutter tool. -include(${EPHEMERAL_DIR}/generated_config.cmake) - -# TODO: Move the rest of this into files in ephemeral. See -# https://github.com/flutter/flutter/issues/57146. -set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") - -# === Flutter Library === -set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") - -# Published to parent scope for install step. -set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) -set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) -set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) -set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE) - -list(APPEND FLUTTER_LIBRARY_HEADERS - "flutter_export.h" - "flutter_windows.h" - "flutter_messenger.h" - "flutter_plugin_registrar.h" - "flutter_texture_registrar.h" -) -list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") -add_library(flutter INTERFACE) -target_include_directories(flutter INTERFACE - "${EPHEMERAL_DIR}" -) -target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") -add_dependencies(flutter flutter_assemble) - -# === Wrapper === -list(APPEND CPP_WRAPPER_SOURCES_CORE - "core_implementations.cc" - "standard_codec.cc" -) -list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") -list(APPEND CPP_WRAPPER_SOURCES_PLUGIN - "plugin_registrar.cc" -) -list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") -list(APPEND CPP_WRAPPER_SOURCES_APP - "flutter_engine.cc" - "flutter_view_controller.cc" -) -list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") - -# Wrapper sources needed for a plugin. -add_library(flutter_wrapper_plugin STATIC - ${CPP_WRAPPER_SOURCES_CORE} - ${CPP_WRAPPER_SOURCES_PLUGIN} -) -apply_standard_settings(flutter_wrapper_plugin) -set_target_properties(flutter_wrapper_plugin PROPERTIES - POSITION_INDEPENDENT_CODE ON) -set_target_properties(flutter_wrapper_plugin PROPERTIES - CXX_VISIBILITY_PRESET hidden) -target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) -target_include_directories(flutter_wrapper_plugin PUBLIC - "${WRAPPER_ROOT}/include" -) -add_dependencies(flutter_wrapper_plugin flutter_assemble) - -# Wrapper sources needed for the runner. -add_library(flutter_wrapper_app STATIC - ${CPP_WRAPPER_SOURCES_CORE} - ${CPP_WRAPPER_SOURCES_APP} -) -apply_standard_settings(flutter_wrapper_app) -target_link_libraries(flutter_wrapper_app PUBLIC flutter) -target_include_directories(flutter_wrapper_app PUBLIC - "${WRAPPER_ROOT}/include" -) -add_dependencies(flutter_wrapper_app flutter_assemble) - -# === Flutter tool backend === -# _phony_ is a non-existent file to force this command to run every time, -# since currently there's no way to get a full input/output list from the -# flutter tool. -set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") -set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) -add_custom_command( - OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} - ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} - ${CPP_WRAPPER_SOURCES_APP} - ${PHONY_OUTPUT} - COMMAND ${CMAKE_COMMAND} -E env - ${FLUTTER_TOOL_ENVIRONMENT} - "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" - windows-x64 $ - VERBATIM -) -add_custom_target(flutter_assemble DEPENDS - "${FLUTTER_LIBRARY}" - ${FLUTTER_LIBRARY_HEADERS} - ${CPP_WRAPPER_SOURCES_CORE} - ${CPP_WRAPPER_SOURCES_PLUGIN} - ${CPP_WRAPPER_SOURCES_APP} -) +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.14) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. +set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") + +# === Flutter Library === +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "flutter_export.h" + "flutter_windows.h" + "flutter_messenger.h" + "flutter_plugin_registrar.h" + "flutter_texture_registrar.h" +) +list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") +add_dependencies(flutter flutter_assemble) + +# === Wrapper === +list(APPEND CPP_WRAPPER_SOURCES_CORE + "core_implementations.cc" + "standard_codec.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_PLUGIN + "plugin_registrar.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_APP + "flutter_engine.cc" + "flutter_view_controller.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") + +# Wrapper sources needed for a plugin. +add_library(flutter_wrapper_plugin STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} +) +apply_standard_settings(flutter_wrapper_plugin) +set_target_properties(flutter_wrapper_plugin PROPERTIES + POSITION_INDEPENDENT_CODE ON) +set_target_properties(flutter_wrapper_plugin PROPERTIES + CXX_VISIBILITY_PRESET hidden) +target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) +target_include_directories(flutter_wrapper_plugin PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_plugin flutter_assemble) + +# Wrapper sources needed for the runner. +add_library(flutter_wrapper_app STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_APP} +) +apply_standard_settings(flutter_wrapper_app) +target_link_libraries(flutter_wrapper_app PUBLIC flutter) +target_include_directories(flutter_wrapper_app PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_app flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") +set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} + ${PHONY_OUTPUT} + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" + windows-x64 $ + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} +) diff --git a/example/windows/flutter/generated_plugin_registrant.cc b/example/windows/flutter/generated_plugin_registrant.cc index bc268640..ef6dc906 100644 --- a/example/windows/flutter/generated_plugin_registrant.cc +++ b/example/windows/flutter/generated_plugin_registrant.cc @@ -6,18 +6,24 @@ #include "generated_plugin_registrant.h" +#include #include #include #include +#include #include void RegisterPlugins(flutter::PluginRegistry* registry) { + DesktopDropPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("DesktopDropPlugin")); FileSelectorWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("FileSelectorWindows")); GalPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("GalPluginCApi")); PasteboardPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("PasteboardPlugin")); + SharePlusWindowsPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi")); UrlLauncherWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("UrlLauncherWindows")); } diff --git a/example/windows/flutter/generated_plugins.cmake b/example/windows/flutter/generated_plugins.cmake index 029549d7..abc6f627 100644 --- a/example/windows/flutter/generated_plugins.cmake +++ b/example/windows/flutter/generated_plugins.cmake @@ -3,9 +3,11 @@ # list(APPEND FLUTTER_PLUGIN_LIST + desktop_drop file_selector_windows gal pasteboard + share_plus url_launcher_windows ) diff --git a/example/windows/runner/CMakeLists.txt b/example/windows/runner/CMakeLists.txt index e9932176..394917c0 100644 --- a/example/windows/runner/CMakeLists.txt +++ b/example/windows/runner/CMakeLists.txt @@ -1,18 +1,40 @@ -cmake_minimum_required(VERSION 3.15) -project(runner LANGUAGES CXX) - -add_executable(${BINARY_NAME} WIN32 - "flutter_window.cpp" - "main.cpp" - "run_loop.cpp" - "utils.cpp" - "win32_window.cpp" - "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" - "Runner.rc" - "runner.exe.manifest" -) -apply_standard_settings(${BINARY_NAME}) -target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") -target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) -target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") -add_dependencies(${BINARY_NAME} flutter_assemble) +cmake_minimum_required(VERSION 3.14) +project(runner LANGUAGES CXX) + +# Define the application target. To change its name, change BINARY_NAME in the +# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer +# work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} WIN32 + "flutter_window.cpp" + "main.cpp" + "utils.cpp" + "win32_window.cpp" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" + "Runner.rc" + "runner.exe.manifest" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add preprocessor definitions for the build version. +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}") + +# Disable Windows macros that collide with C++ standard library functions. +target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") + +# Add dependency libraries and include directories. Add any application-specific +# dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) +target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib") +target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) diff --git a/example/windows/runner/Runner.rc b/example/windows/runner/Runner.rc index a922e84e..aecaa2b5 100644 --- a/example/windows/runner/Runner.rc +++ b/example/windows/runner/Runner.rc @@ -1,121 +1,121 @@ -// Microsoft Visual C++ generated resource script. -// -#pragma code_page(65001) -#include "resource.h" - -#define APSTUDIO_READONLY_SYMBOLS -///////////////////////////////////////////////////////////////////////////// -// -// Generated from the TEXTINCLUDE 2 resource. -// -#include "winres.h" - -///////////////////////////////////////////////////////////////////////////// -#undef APSTUDIO_READONLY_SYMBOLS - -///////////////////////////////////////////////////////////////////////////// -// English (United States) resources - -#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) -LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US - -#ifdef APSTUDIO_INVOKED -///////////////////////////////////////////////////////////////////////////// -// -// TEXTINCLUDE -// - -1 TEXTINCLUDE -BEGIN - "resource.h\0" -END - -2 TEXTINCLUDE -BEGIN - "#include ""winres.h""\r\n" - "\0" -END - -3 TEXTINCLUDE -BEGIN - "\r\n" - "\0" -END - -#endif // APSTUDIO_INVOKED - - -///////////////////////////////////////////////////////////////////////////// -// -// Icon -// - -// Icon with lowest ID value placed first to ensure application icon -// remains consistent on all systems. -IDI_APP_ICON ICON "resources\\app_icon.ico" - - -///////////////////////////////////////////////////////////////////////////// -// -// Version -// - -#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD) -#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD -#else -#define VERSION_AS_NUMBER 1,0,0,0 -#endif - -#if defined(FLUTTER_VERSION) -#define VERSION_AS_STRING FLUTTER_VERSION -#else -#define VERSION_AS_STRING "1.0.0" -#endif - -VS_VERSION_INFO VERSIONINFO - FILEVERSION VERSION_AS_NUMBER - PRODUCTVERSION VERSION_AS_NUMBER - FILEFLAGSMASK VS_FFI_FILEFLAGSMASK -#ifdef _DEBUG - FILEFLAGS VS_FF_DEBUG -#else - FILEFLAGS 0x0L -#endif - FILEOS VOS__WINDOWS32 - FILETYPE VFT_APP - FILESUBTYPE 0x0L -BEGIN - BLOCK "StringFileInfo" - BEGIN - BLOCK "040904e4" - BEGIN - VALUE "CompanyName", "com.example" "\0" - VALUE "FileDescription", "A new Flutter project." "\0" - VALUE "FileVersion", VERSION_AS_STRING "\0" - VALUE "InternalName", "app" "\0" - VALUE "LegalCopyright", "Copyright (C) 2021 com.example. All rights reserved." "\0" - VALUE "OriginalFilename", "app.exe" "\0" - VALUE "ProductName", "app" "\0" - VALUE "ProductVersion", VERSION_AS_STRING "\0" - END - END - BLOCK "VarFileInfo" - BEGIN - VALUE "Translation", 0x409, 1252 - END -END - -#endif // English (United States) resources -///////////////////////////////////////////////////////////////////////////// - - - -#ifndef APSTUDIO_INVOKED -///////////////////////////////////////////////////////////////////////////// -// -// Generated from the TEXTINCLUDE 3 resource. -// - - -///////////////////////////////////////////////////////////////////////////// -#endif // not APSTUDIO_INVOKED +// Microsoft Visual C++ generated resource script. +// +#pragma code_page(65001) +#include "resource.h" + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#include "winres.h" + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// English (United States) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE +BEGIN + "resource.h\0" +END + +2 TEXTINCLUDE +BEGIN + "#include ""winres.h""\r\n" + "\0" +END + +3 TEXTINCLUDE +BEGIN + "\r\n" + "\0" +END + +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// Icon +// + +// Icon with lowest ID value placed first to ensure application icon +// remains consistent on all systems. +IDI_APP_ICON ICON "resources\\app_icon.ico" + + +///////////////////////////////////////////////////////////////////////////// +// +// Version +// + +#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD) +#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD +#else +#define VERSION_AS_NUMBER 1,0,0,0 +#endif + +#if defined(FLUTTER_VERSION) +#define VERSION_AS_STRING FLUTTER_VERSION +#else +#define VERSION_AS_STRING "1.0.0" +#endif + +VS_VERSION_INFO VERSIONINFO + FILEVERSION VERSION_AS_NUMBER + PRODUCTVERSION VERSION_AS_NUMBER + FILEFLAGSMASK VS_FFI_FILEFLAGSMASK +#ifdef _DEBUG + FILEFLAGS VS_FF_DEBUG +#else + FILEFLAGS 0x0L +#endif + FILEOS VOS__WINDOWS32 + FILETYPE VFT_APP + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904e4" + BEGIN + VALUE "CompanyName", "com.example" "\0" + VALUE "FileDescription", "example" "\0" + VALUE "FileVersion", VERSION_AS_STRING "\0" + VALUE "InternalName", "example" "\0" + VALUE "LegalCopyright", "Copyright (C) 2023 com.example. All rights reserved." "\0" + VALUE "OriginalFilename", "example.exe" "\0" + VALUE "ProductName", "example" "\0" + VALUE "ProductVersion", VERSION_AS_STRING "\0" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1252 + END +END + +#endif // English (United States) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED diff --git a/example/windows/runner/flutter_window.cpp b/example/windows/runner/flutter_window.cpp index ac04f779..955ee303 100644 --- a/example/windows/runner/flutter_window.cpp +++ b/example/windows/runner/flutter_window.cpp @@ -1,64 +1,71 @@ -#include "flutter_window.h" - -#include - -#include "flutter/generated_plugin_registrant.h" - -FlutterWindow::FlutterWindow(RunLoop* run_loop, - const flutter::DartProject& project) - : run_loop_(run_loop), project_(project) {} - -FlutterWindow::~FlutterWindow() {} - -bool FlutterWindow::OnCreate() { - if (!Win32Window::OnCreate()) { - return false; - } - - RECT frame = GetClientArea(); - - // The size here must match the window dimensions to avoid unnecessary surface - // creation / destruction in the startup path. - flutter_controller_ = std::make_unique( - frame.right - frame.left, frame.bottom - frame.top, project_); - // Ensure that basic setup of the controller was successful. - if (!flutter_controller_->engine() || !flutter_controller_->view()) { - return false; - } - RegisterPlugins(flutter_controller_->engine()); - run_loop_->RegisterFlutterInstance(flutter_controller_->engine()); - SetChildContent(flutter_controller_->view()->GetNativeWindow()); - return true; -} - -void FlutterWindow::OnDestroy() { - if (flutter_controller_) { - run_loop_->UnregisterFlutterInstance(flutter_controller_->engine()); - flutter_controller_ = nullptr; - } - - Win32Window::OnDestroy(); -} - -LRESULT -FlutterWindow::MessageHandler(HWND hwnd, UINT const message, - WPARAM const wparam, - LPARAM const lparam) noexcept { - // Give Flutter, including plugins, an opporutunity to handle window messages. - if (flutter_controller_) { - std::optional result = - flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, - lparam); - if (result) { - return *result; - } - } - - switch (message) { - case WM_FONTCHANGE: - flutter_controller_->engine()->ReloadSystemFonts(); - break; - } - - return Win32Window::MessageHandler(hwnd, message, wparam, lparam); -} +#include "flutter_window.h" + +#include + +#include "flutter/generated_plugin_registrant.h" + +FlutterWindow::FlutterWindow(const flutter::DartProject& project) + : project_(project) {} + +FlutterWindow::~FlutterWindow() {} + +bool FlutterWindow::OnCreate() { + if (!Win32Window::OnCreate()) { + return false; + } + + RECT frame = GetClientArea(); + + // The size here must match the window dimensions to avoid unnecessary surface + // creation / destruction in the startup path. + flutter_controller_ = std::make_unique( + frame.right - frame.left, frame.bottom - frame.top, project_); + // Ensure that basic setup of the controller was successful. + if (!flutter_controller_->engine() || !flutter_controller_->view()) { + return false; + } + RegisterPlugins(flutter_controller_->engine()); + SetChildContent(flutter_controller_->view()->GetNativeWindow()); + + flutter_controller_->engine()->SetNextFrameCallback([&]() { + this->Show(); + }); + + // Flutter can complete the first frame before the "show window" callback is + // registered. The following call ensures a frame is pending to ensure the + // window is shown. It is a no-op if the first frame hasn't completed yet. + flutter_controller_->ForceRedraw(); + + return true; +} + +void FlutterWindow::OnDestroy() { + if (flutter_controller_) { + flutter_controller_ = nullptr; + } + + Win32Window::OnDestroy(); +} + +LRESULT +FlutterWindow::MessageHandler(HWND hwnd, UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + // Give Flutter, including plugins, an opportunity to handle window messages. + if (flutter_controller_) { + std::optional result = + flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, + lparam); + if (result) { + return *result; + } + } + + switch (message) { + case WM_FONTCHANGE: + flutter_controller_->engine()->ReloadSystemFonts(); + break; + } + + return Win32Window::MessageHandler(hwnd, message, wparam, lparam); +} diff --git a/example/windows/runner/flutter_window.h b/example/windows/runner/flutter_window.h index ba86031c..6da0652f 100644 --- a/example/windows/runner/flutter_window.h +++ b/example/windows/runner/flutter_window.h @@ -1,39 +1,33 @@ -#ifndef RUNNER_FLUTTER_WINDOW_H_ -#define RUNNER_FLUTTER_WINDOW_H_ - -#include -#include - -#include - -#include "run_loop.h" -#include "win32_window.h" - -// A window that does nothing but host a Flutter view. -class FlutterWindow : public Win32Window { - public: - // Creates a new FlutterWindow driven by the |run_loop|, hosting a - // Flutter view running |project|. - explicit FlutterWindow(RunLoop* run_loop, - const flutter::DartProject& project); - virtual ~FlutterWindow(); - - protected: - // Win32Window: - bool OnCreate() override; - void OnDestroy() override; - LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, - LPARAM const lparam) noexcept override; - - private: - // The run loop driving events for this window. - RunLoop* run_loop_; - - // The project to run. - flutter::DartProject project_; - - // The Flutter instance hosted by this window. - std::unique_ptr flutter_controller_; -}; - -#endif // RUNNER_FLUTTER_WINDOW_H_ +#ifndef RUNNER_FLUTTER_WINDOW_H_ +#define RUNNER_FLUTTER_WINDOW_H_ + +#include +#include + +#include + +#include "win32_window.h" + +// A window that does nothing but host a Flutter view. +class FlutterWindow : public Win32Window { + public: + // Creates a new FlutterWindow hosting a Flutter view running |project|. + explicit FlutterWindow(const flutter::DartProject& project); + virtual ~FlutterWindow(); + + protected: + // Win32Window: + bool OnCreate() override; + void OnDestroy() override; + LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, + LPARAM const lparam) noexcept override; + + private: + // The project to run. + flutter::DartProject project_; + + // The Flutter instance hosted by this window. + std::unique_ptr flutter_controller_; +}; + +#endif // RUNNER_FLUTTER_WINDOW_H_ diff --git a/example/windows/runner/main.cpp b/example/windows/runner/main.cpp index 81da1d1c..a61bf80d 100644 --- a/example/windows/runner/main.cpp +++ b/example/windows/runner/main.cpp @@ -1,42 +1,43 @@ -#include -#include -#include - -#include "flutter_window.h" -#include "run_loop.h" -#include "utils.h" - -int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, - _In_ wchar_t *command_line, _In_ int show_command) { - // Attach to console when present (e.g., 'flutter run') or create a - // new console when running with a debugger. - if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { - CreateAndAttachConsole(); - } - - // Initialize COM, so that it is available for use in the library and/or - // plugins. - ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); - - RunLoop run_loop; - - flutter::DartProject project(L"data"); - - std::vector command_line_arguments = - GetCommandLineArguments(); - - project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); - - FlutterWindow window(&run_loop, project); - Win32Window::Point origin(10, 10); - Win32Window::Size size(1280, 720); - if (!window.CreateAndShow(L"app", origin, size)) { - return EXIT_FAILURE; - } - window.SetQuitOnClose(true); - - run_loop.Run(); - - ::CoUninitialize(); - return EXIT_SUCCESS; -} +#include +#include +#include + +#include "flutter_window.h" +#include "utils.h" + +int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, + _In_ wchar_t *command_line, _In_ int show_command) { + // Attach to console when present (e.g., 'flutter run') or create a + // new console when running with a debugger. + if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { + CreateAndAttachConsole(); + } + + // Initialize COM, so that it is available for use in the library and/or + // plugins. + ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); + + flutter::DartProject project(L"data"); + + std::vector command_line_arguments = + GetCommandLineArguments(); + + project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); + + FlutterWindow window(project); + Win32Window::Point origin(10, 10); + Win32Window::Size size(1280, 720); + if (!window.Create(L"example", origin, size)) { + return EXIT_FAILURE; + } + window.SetQuitOnClose(true); + + ::MSG msg; + while (::GetMessage(&msg, nullptr, 0, 0)) { + ::TranslateMessage(&msg); + ::DispatchMessage(&msg); + } + + ::CoUninitialize(); + return EXIT_SUCCESS; +} diff --git a/example/windows/runner/resource.h b/example/windows/runner/resource.h index ddc7f3ef..66a65d1e 100644 --- a/example/windows/runner/resource.h +++ b/example/windows/runner/resource.h @@ -1,16 +1,16 @@ -//{{NO_DEPENDENCIES}} -// Microsoft Visual C++ generated include file. -// Used by Runner.rc -// -#define IDI_APP_ICON 101 - -// Next default values for new objects -// -#ifdef APSTUDIO_INVOKED -#ifndef APSTUDIO_READONLY_SYMBOLS -#define _APS_NEXT_RESOURCE_VALUE 102 -#define _APS_NEXT_COMMAND_VALUE 40001 -#define _APS_NEXT_CONTROL_VALUE 1001 -#define _APS_NEXT_SYMED_VALUE 101 -#endif -#endif +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by Runner.rc +// +#define IDI_APP_ICON 101 + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 102 +#define _APS_NEXT_COMMAND_VALUE 40001 +#define _APS_NEXT_CONTROL_VALUE 1001 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif diff --git a/example/windows/runner/run_loop.cpp b/example/windows/runner/run_loop.cpp deleted file mode 100644 index 0d912118..00000000 --- a/example/windows/runner/run_loop.cpp +++ /dev/null @@ -1,66 +0,0 @@ -#include "run_loop.h" - -#include - -#include - -RunLoop::RunLoop() {} - -RunLoop::~RunLoop() {} - -void RunLoop::Run() { - bool keep_running = true; - TimePoint next_flutter_event_time = TimePoint::clock::now(); - while (keep_running) { - std::chrono::nanoseconds wait_duration = - std::max(std::chrono::nanoseconds(0), - next_flutter_event_time - TimePoint::clock::now()); - ::MsgWaitForMultipleObjects( - 0, nullptr, FALSE, static_cast(wait_duration.count() / 1000), - QS_ALLINPUT); - bool processed_events = false; - MSG message; - // All pending Windows messages must be processed; MsgWaitForMultipleObjects - // won't return again for items left in the queue after PeekMessage. - while (::PeekMessage(&message, nullptr, 0, 0, PM_REMOVE)) { - processed_events = true; - if (message.message == WM_QUIT) { - keep_running = false; - break; - } - ::TranslateMessage(&message); - ::DispatchMessage(&message); - // Allow Flutter to process messages each time a Windows message is - // processed, to prevent starvation. - next_flutter_event_time = - std::min(next_flutter_event_time, ProcessFlutterMessages()); - } - // If the PeekMessage loop didn't run, process Flutter messages. - if (!processed_events) { - next_flutter_event_time = - std::min(next_flutter_event_time, ProcessFlutterMessages()); - } - } -} - -void RunLoop::RegisterFlutterInstance( - flutter::FlutterEngine* flutter_instance) { - flutter_instances_.insert(flutter_instance); -} - -void RunLoop::UnregisterFlutterInstance( - flutter::FlutterEngine* flutter_instance) { - flutter_instances_.erase(flutter_instance); -} - -RunLoop::TimePoint RunLoop::ProcessFlutterMessages() { - TimePoint next_event_time = TimePoint::max(); - for (auto instance : flutter_instances_) { - std::chrono::nanoseconds wait_duration = instance->ProcessMessages(); - if (wait_duration != std::chrono::nanoseconds::max()) { - next_event_time = - std::min(next_event_time, TimePoint::clock::now() + wait_duration); - } - } - return next_event_time; -} diff --git a/example/windows/runner/run_loop.h b/example/windows/runner/run_loop.h deleted file mode 100644 index 54927f97..00000000 --- a/example/windows/runner/run_loop.h +++ /dev/null @@ -1,40 +0,0 @@ -#ifndef RUNNER_RUN_LOOP_H_ -#define RUNNER_RUN_LOOP_H_ - -#include - -#include -#include - -// A runloop that will service events for Flutter instances as well -// as native messages. -class RunLoop { - public: - RunLoop(); - ~RunLoop(); - - // Prevent copying - RunLoop(RunLoop const&) = delete; - RunLoop& operator=(RunLoop const&) = delete; - - // Runs the run loop until the application quits. - void Run(); - - // Registers the given Flutter instance for event servicing. - void RegisterFlutterInstance( - flutter::FlutterEngine* flutter_instance); - - // Unregisters the given Flutter instance from event servicing. - void UnregisterFlutterInstance( - flutter::FlutterEngine* flutter_instance); - - private: - using TimePoint = std::chrono::steady_clock::time_point; - - // Processes all currently pending messages for registered Flutter instances. - TimePoint ProcessFlutterMessages(); - - std::set flutter_instances_; -}; - -#endif // RUNNER_RUN_LOOP_H_ diff --git a/example/windows/runner/runner.exe.manifest b/example/windows/runner/runner.exe.manifest index 2c680b8b..a42ea768 100644 --- a/example/windows/runner/runner.exe.manifest +++ b/example/windows/runner/runner.exe.manifest @@ -1,20 +1,20 @@ - - - - - PerMonitorV2 - - - - - - - - - - - - - - - + + + + + PerMonitorV2 + + + + + + + + + + + + + + + diff --git a/example/windows/runner/utils.cpp b/example/windows/runner/utils.cpp index 05b53c01..b2b08734 100644 --- a/example/windows/runner/utils.cpp +++ b/example/windows/runner/utils.cpp @@ -1,64 +1,65 @@ -#include "utils.h" - -#include -#include -#include -#include - -#include - -void CreateAndAttachConsole() { - if (::AllocConsole()) { - FILE *unused; - if (freopen_s(&unused, "CONOUT$", "w", stdout)) { - _dup2(_fileno(stdout), 1); - } - if (freopen_s(&unused, "CONOUT$", "w", stderr)) { - _dup2(_fileno(stdout), 2); - } - std::ios::sync_with_stdio(); - FlutterDesktopResyncOutputStreams(); - } -} - -std::vector GetCommandLineArguments() { - // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. - int argc; - wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); - if (argv == nullptr) { - return std::vector(); - } - - std::vector command_line_arguments; - - // Skip the first argument as it's the binary name. - for (int i = 1; i < argc; i++) { - command_line_arguments.push_back(Utf8FromUtf16(argv[i])); - } - - ::LocalFree(argv); - - return command_line_arguments; -} - -std::string Utf8FromUtf16(const wchar_t* utf16_string) { - if (utf16_string == nullptr) { - return std::string(); - } - int target_length = ::WideCharToMultiByte( - CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, - -1, nullptr, 0, nullptr, nullptr); - if (target_length == 0) { - return std::string(); - } - std::string utf8_string; - utf8_string.resize(target_length); - int converted_length = ::WideCharToMultiByte( - CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, - -1, utf8_string.data(), - target_length, nullptr, nullptr); - if (converted_length == 0) { - return std::string(); - } - return utf8_string; -} +#include "utils.h" + +#include +#include +#include +#include + +#include + +void CreateAndAttachConsole() { + if (::AllocConsole()) { + FILE *unused; + if (freopen_s(&unused, "CONOUT$", "w", stdout)) { + _dup2(_fileno(stdout), 1); + } + if (freopen_s(&unused, "CONOUT$", "w", stderr)) { + _dup2(_fileno(stdout), 2); + } + std::ios::sync_with_stdio(); + FlutterDesktopResyncOutputStreams(); + } +} + +std::vector GetCommandLineArguments() { + // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. + int argc; + wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); + if (argv == nullptr) { + return std::vector(); + } + + std::vector command_line_arguments; + + // Skip the first argument as it's the binary name. + for (int i = 1; i < argc; i++) { + command_line_arguments.push_back(Utf8FromUtf16(argv[i])); + } + + ::LocalFree(argv); + + return command_line_arguments; +} + +std::string Utf8FromUtf16(const wchar_t* utf16_string) { + if (utf16_string == nullptr) { + return std::string(); + } + int target_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + -1, nullptr, 0, nullptr, nullptr) + -1; // remove the trailing null character + int input_length = (int)wcslen(utf16_string); + std::string utf8_string; + if (target_length <= 0 || target_length > utf8_string.max_size()) { + return utf8_string; + } + utf8_string.resize(target_length); + int converted_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + input_length, utf8_string.data(), target_length, nullptr, nullptr); + if (converted_length == 0) { + return std::string(); + } + return utf8_string; +} diff --git a/example/windows/runner/utils.h b/example/windows/runner/utils.h index 3f0e05cb..3879d547 100644 --- a/example/windows/runner/utils.h +++ b/example/windows/runner/utils.h @@ -1,19 +1,19 @@ -#ifndef RUNNER_UTILS_H_ -#define RUNNER_UTILS_H_ - -#include -#include - -// Creates a console for the process, and redirects stdout and stderr to -// it for both the runner and the Flutter library. -void CreateAndAttachConsole(); - -// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string -// encoded in UTF-8. Returns an empty std::string on failure. -std::string Utf8FromUtf16(const wchar_t* utf16_string); - -// Gets the command line arguments passed in as a std::vector, -// encoded in UTF-8. Returns an empty std::vector on failure. -std::vector GetCommandLineArguments(); - -#endif // RUNNER_UTILS_H_ +#ifndef RUNNER_UTILS_H_ +#define RUNNER_UTILS_H_ + +#include +#include + +// Creates a console for the process, and redirects stdout and stderr to +// it for both the runner and the Flutter library. +void CreateAndAttachConsole(); + +// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string +// encoded in UTF-8. Returns an empty std::string on failure. +std::string Utf8FromUtf16(const wchar_t* utf16_string); + +// Gets the command line arguments passed in as a std::vector, +// encoded in UTF-8. Returns an empty std::vector on failure. +std::vector GetCommandLineArguments(); + +#endif // RUNNER_UTILS_H_ diff --git a/example/windows/runner/win32_window.cpp b/example/windows/runner/win32_window.cpp index 97f4439c..60608d0f 100644 --- a/example/windows/runner/win32_window.cpp +++ b/example/windows/runner/win32_window.cpp @@ -1,245 +1,288 @@ -#include "win32_window.h" - -#include - -#include "resource.h" - -namespace { - -constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; - -// The number of Win32Window objects that currently exist. -static int g_active_window_count = 0; - -using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd); - -// Scale helper to convert logical scaler values to physical using passed in -// scale factor -int Scale(int source, double scale_factor) { - return static_cast(source * scale_factor); -} - -// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module. -// This API is only needed for PerMonitor V1 awareness mode. -void EnableFullDpiSupportIfAvailable(HWND hwnd) { - HMODULE user32_module = LoadLibraryA("User32.dll"); - if (!user32_module) { - return; - } - auto enable_non_client_dpi_scaling = - reinterpret_cast( - GetProcAddress(user32_module, "EnableNonClientDpiScaling")); - if (enable_non_client_dpi_scaling != nullptr) { - enable_non_client_dpi_scaling(hwnd); - FreeLibrary(user32_module); - } -} - -} // namespace - -// Manages the Win32Window's window class registration. -class WindowClassRegistrar { - public: - ~WindowClassRegistrar() = default; - - // Returns the singleton registar instance. - static WindowClassRegistrar* GetInstance() { - if (!instance_) { - instance_ = new WindowClassRegistrar(); - } - return instance_; - } - - // Returns the name of the window class, registering the class if it hasn't - // previously been registered. - const wchar_t* GetWindowClass(); - - // Unregisters the window class. Should only be called if there are no - // instances of the window. - void UnregisterWindowClass(); - - private: - WindowClassRegistrar() = default; - - static WindowClassRegistrar* instance_; - - bool class_registered_ = false; -}; - -WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr; - -const wchar_t* WindowClassRegistrar::GetWindowClass() { - if (!class_registered_) { - WNDCLASS window_class{}; - window_class.hCursor = LoadCursor(nullptr, IDC_ARROW); - window_class.lpszClassName = kWindowClassName; - window_class.style = CS_HREDRAW | CS_VREDRAW; - window_class.cbClsExtra = 0; - window_class.cbWndExtra = 0; - window_class.hInstance = GetModuleHandle(nullptr); - window_class.hIcon = - LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON)); - window_class.hbrBackground = 0; - window_class.lpszMenuName = nullptr; - window_class.lpfnWndProc = Win32Window::WndProc; - RegisterClass(&window_class); - class_registered_ = true; - } - return kWindowClassName; -} - -void WindowClassRegistrar::UnregisterWindowClass() { - UnregisterClass(kWindowClassName, nullptr); - class_registered_ = false; -} - -Win32Window::Win32Window() { - ++g_active_window_count; -} - -Win32Window::~Win32Window() { - --g_active_window_count; - Destroy(); -} - -bool Win32Window::CreateAndShow(const std::wstring& title, - const Point& origin, - const Size& size) { - Destroy(); - - const wchar_t* window_class = - WindowClassRegistrar::GetInstance()->GetWindowClass(); - - const POINT target_point = {static_cast(origin.x), - static_cast(origin.y)}; - HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST); - UINT dpi = FlutterDesktopGetDpiForMonitor(monitor); - double scale_factor = dpi / 96.0; - - HWND window = CreateWindow( - window_class, title.c_str(), WS_OVERLAPPEDWINDOW | WS_VISIBLE, - Scale(origin.x, scale_factor), Scale(origin.y, scale_factor), - Scale(size.width, scale_factor), Scale(size.height, scale_factor), - nullptr, nullptr, GetModuleHandle(nullptr), this); - - if (!window) { - return false; - } - - return OnCreate(); -} - -// static -LRESULT CALLBACK Win32Window::WndProc(HWND const window, - UINT const message, - WPARAM const wparam, - LPARAM const lparam) noexcept { - if (message == WM_NCCREATE) { - auto window_struct = reinterpret_cast(lparam); - SetWindowLongPtr(window, GWLP_USERDATA, - reinterpret_cast(window_struct->lpCreateParams)); - - auto that = static_cast(window_struct->lpCreateParams); - EnableFullDpiSupportIfAvailable(window); - that->window_handle_ = window; - } else if (Win32Window* that = GetThisFromHandle(window)) { - return that->MessageHandler(window, message, wparam, lparam); - } - - return DefWindowProc(window, message, wparam, lparam); -} - -LRESULT -Win32Window::MessageHandler(HWND hwnd, - UINT const message, - WPARAM const wparam, - LPARAM const lparam) noexcept { - switch (message) { - case WM_DESTROY: - window_handle_ = nullptr; - Destroy(); - if (quit_on_close_) { - PostQuitMessage(0); - } - return 0; - - case WM_DPICHANGED: { - auto newRectSize = reinterpret_cast(lparam); - LONG newWidth = newRectSize->right - newRectSize->left; - LONG newHeight = newRectSize->bottom - newRectSize->top; - - SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth, - newHeight, SWP_NOZORDER | SWP_NOACTIVATE); - - return 0; - } - case WM_SIZE: { - RECT rect = GetClientArea(); - if (child_content_ != nullptr) { - // Size and position the child window. - MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left, - rect.bottom - rect.top, TRUE); - } - return 0; - } - - case WM_ACTIVATE: - if (child_content_ != nullptr) { - SetFocus(child_content_); - } - return 0; - } - - return DefWindowProc(window_handle_, message, wparam, lparam); -} - -void Win32Window::Destroy() { - OnDestroy(); - - if (window_handle_) { - DestroyWindow(window_handle_); - window_handle_ = nullptr; - } - if (g_active_window_count == 0) { - WindowClassRegistrar::GetInstance()->UnregisterWindowClass(); - } -} - -Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept { - return reinterpret_cast( - GetWindowLongPtr(window, GWLP_USERDATA)); -} - -void Win32Window::SetChildContent(HWND content) { - child_content_ = content; - SetParent(content, window_handle_); - RECT frame = GetClientArea(); - - MoveWindow(content, frame.left, frame.top, frame.right - frame.left, - frame.bottom - frame.top, true); - - SetFocus(child_content_); -} - -RECT Win32Window::GetClientArea() { - RECT frame; - GetClientRect(window_handle_, &frame); - return frame; -} - -HWND Win32Window::GetHandle() { - return window_handle_; -} - -void Win32Window::SetQuitOnClose(bool quit_on_close) { - quit_on_close_ = quit_on_close; -} - -bool Win32Window::OnCreate() { - // No-op; provided for subclasses. - return true; -} - -void Win32Window::OnDestroy() { - // No-op; provided for subclasses. -} +#include "win32_window.h" + +#include +#include + +#include "resource.h" + +namespace { + +/// Window attribute that enables dark mode window decorations. +/// +/// Redefined in case the developer's machine has a Windows SDK older than +/// version 10.0.22000.0. +/// See: https://docs.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute +#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE +#define DWMWA_USE_IMMERSIVE_DARK_MODE 20 +#endif + +constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; + +/// Registry key for app theme preference. +/// +/// A value of 0 indicates apps should use dark mode. A non-zero or missing +/// value indicates apps should use light mode. +constexpr const wchar_t kGetPreferredBrightnessRegKey[] = + L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize"; +constexpr const wchar_t kGetPreferredBrightnessRegValue[] = L"AppsUseLightTheme"; + +// The number of Win32Window objects that currently exist. +static int g_active_window_count = 0; + +using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd); + +// Scale helper to convert logical scaler values to physical using passed in +// scale factor +int Scale(int source, double scale_factor) { + return static_cast(source * scale_factor); +} + +// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module. +// This API is only needed for PerMonitor V1 awareness mode. +void EnableFullDpiSupportIfAvailable(HWND hwnd) { + HMODULE user32_module = LoadLibraryA("User32.dll"); + if (!user32_module) { + return; + } + auto enable_non_client_dpi_scaling = + reinterpret_cast( + GetProcAddress(user32_module, "EnableNonClientDpiScaling")); + if (enable_non_client_dpi_scaling != nullptr) { + enable_non_client_dpi_scaling(hwnd); + } + FreeLibrary(user32_module); +} + +} // namespace + +// Manages the Win32Window's window class registration. +class WindowClassRegistrar { + public: + ~WindowClassRegistrar() = default; + + // Returns the singleton registrar instance. + static WindowClassRegistrar* GetInstance() { + if (!instance_) { + instance_ = new WindowClassRegistrar(); + } + return instance_; + } + + // Returns the name of the window class, registering the class if it hasn't + // previously been registered. + const wchar_t* GetWindowClass(); + + // Unregisters the window class. Should only be called if there are no + // instances of the window. + void UnregisterWindowClass(); + + private: + WindowClassRegistrar() = default; + + static WindowClassRegistrar* instance_; + + bool class_registered_ = false; +}; + +WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr; + +const wchar_t* WindowClassRegistrar::GetWindowClass() { + if (!class_registered_) { + WNDCLASS window_class{}; + window_class.hCursor = LoadCursor(nullptr, IDC_ARROW); + window_class.lpszClassName = kWindowClassName; + window_class.style = CS_HREDRAW | CS_VREDRAW; + window_class.cbClsExtra = 0; + window_class.cbWndExtra = 0; + window_class.hInstance = GetModuleHandle(nullptr); + window_class.hIcon = + LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON)); + window_class.hbrBackground = 0; + window_class.lpszMenuName = nullptr; + window_class.lpfnWndProc = Win32Window::WndProc; + RegisterClass(&window_class); + class_registered_ = true; + } + return kWindowClassName; +} + +void WindowClassRegistrar::UnregisterWindowClass() { + UnregisterClass(kWindowClassName, nullptr); + class_registered_ = false; +} + +Win32Window::Win32Window() { + ++g_active_window_count; +} + +Win32Window::~Win32Window() { + --g_active_window_count; + Destroy(); +} + +bool Win32Window::Create(const std::wstring& title, + const Point& origin, + const Size& size) { + Destroy(); + + const wchar_t* window_class = + WindowClassRegistrar::GetInstance()->GetWindowClass(); + + const POINT target_point = {static_cast(origin.x), + static_cast(origin.y)}; + HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST); + UINT dpi = FlutterDesktopGetDpiForMonitor(monitor); + double scale_factor = dpi / 96.0; + + HWND window = CreateWindow( + window_class, title.c_str(), WS_OVERLAPPEDWINDOW, + Scale(origin.x, scale_factor), Scale(origin.y, scale_factor), + Scale(size.width, scale_factor), Scale(size.height, scale_factor), + nullptr, nullptr, GetModuleHandle(nullptr), this); + + if (!window) { + return false; + } + + UpdateTheme(window); + + return OnCreate(); +} + +bool Win32Window::Show() { + return ShowWindow(window_handle_, SW_SHOWNORMAL); +} + +// static +LRESULT CALLBACK Win32Window::WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + if (message == WM_NCCREATE) { + auto window_struct = reinterpret_cast(lparam); + SetWindowLongPtr(window, GWLP_USERDATA, + reinterpret_cast(window_struct->lpCreateParams)); + + auto that = static_cast(window_struct->lpCreateParams); + EnableFullDpiSupportIfAvailable(window); + that->window_handle_ = window; + } else if (Win32Window* that = GetThisFromHandle(window)) { + return that->MessageHandler(window, message, wparam, lparam); + } + + return DefWindowProc(window, message, wparam, lparam); +} + +LRESULT +Win32Window::MessageHandler(HWND hwnd, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + switch (message) { + case WM_DESTROY: + window_handle_ = nullptr; + Destroy(); + if (quit_on_close_) { + PostQuitMessage(0); + } + return 0; + + case WM_DPICHANGED: { + auto newRectSize = reinterpret_cast(lparam); + LONG newWidth = newRectSize->right - newRectSize->left; + LONG newHeight = newRectSize->bottom - newRectSize->top; + + SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth, + newHeight, SWP_NOZORDER | SWP_NOACTIVATE); + + return 0; + } + case WM_SIZE: { + RECT rect = GetClientArea(); + if (child_content_ != nullptr) { + // Size and position the child window. + MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left, + rect.bottom - rect.top, TRUE); + } + return 0; + } + + case WM_ACTIVATE: + if (child_content_ != nullptr) { + SetFocus(child_content_); + } + return 0; + + case WM_DWMCOLORIZATIONCOLORCHANGED: + UpdateTheme(hwnd); + return 0; + } + + return DefWindowProc(window_handle_, message, wparam, lparam); +} + +void Win32Window::Destroy() { + OnDestroy(); + + if (window_handle_) { + DestroyWindow(window_handle_); + window_handle_ = nullptr; + } + if (g_active_window_count == 0) { + WindowClassRegistrar::GetInstance()->UnregisterWindowClass(); + } +} + +Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept { + return reinterpret_cast( + GetWindowLongPtr(window, GWLP_USERDATA)); +} + +void Win32Window::SetChildContent(HWND content) { + child_content_ = content; + SetParent(content, window_handle_); + RECT frame = GetClientArea(); + + MoveWindow(content, frame.left, frame.top, frame.right - frame.left, + frame.bottom - frame.top, true); + + SetFocus(child_content_); +} + +RECT Win32Window::GetClientArea() { + RECT frame; + GetClientRect(window_handle_, &frame); + return frame; +} + +HWND Win32Window::GetHandle() { + return window_handle_; +} + +void Win32Window::SetQuitOnClose(bool quit_on_close) { + quit_on_close_ = quit_on_close; +} + +bool Win32Window::OnCreate() { + // No-op; provided for subclasses. + return true; +} + +void Win32Window::OnDestroy() { + // No-op; provided for subclasses. +} + +void Win32Window::UpdateTheme(HWND const window) { + DWORD light_mode; + DWORD light_mode_size = sizeof(light_mode); + LSTATUS result = RegGetValue(HKEY_CURRENT_USER, kGetPreferredBrightnessRegKey, + kGetPreferredBrightnessRegValue, + RRF_RT_REG_DWORD, nullptr, &light_mode, + &light_mode_size); + + if (result == ERROR_SUCCESS) { + BOOL enable_dark_mode = light_mode == 0; + DwmSetWindowAttribute(window, DWMWA_USE_IMMERSIVE_DARK_MODE, + &enable_dark_mode, sizeof(enable_dark_mode)); + } +} diff --git a/example/windows/runner/win32_window.h b/example/windows/runner/win32_window.h index d9bcac1b..e901dde6 100644 --- a/example/windows/runner/win32_window.h +++ b/example/windows/runner/win32_window.h @@ -1,98 +1,102 @@ -#ifndef RUNNER_WIN32_WINDOW_H_ -#define RUNNER_WIN32_WINDOW_H_ - -#include - -#include -#include -#include - -// A class abstraction for a high DPI-aware Win32 Window. Intended to be -// inherited from by classes that wish to specialize with custom -// rendering and input handling -class Win32Window { - public: - struct Point { - unsigned int x; - unsigned int y; - Point(unsigned int x, unsigned int y) : x(x), y(y) {} - }; - - struct Size { - unsigned int width; - unsigned int height; - Size(unsigned int width, unsigned int height) - : width(width), height(height) {} - }; - - Win32Window(); - virtual ~Win32Window(); - - // Creates and shows a win32 window with |title| and position and size using - // |origin| and |size|. New windows are created on the default monitor. Window - // sizes are specified to the OS in physical pixels, hence to ensure a - // consistent size to will treat the width height passed in to this function - // as logical pixels and scale to appropriate for the default monitor. Returns - // true if the window was created successfully. - bool CreateAndShow(const std::wstring& title, - const Point& origin, - const Size& size); - - // Release OS resources associated with window. - void Destroy(); - - // Inserts |content| into the window tree. - void SetChildContent(HWND content); - - // Returns the backing Window handle to enable clients to set icon and other - // window properties. Returns nullptr if the window has been destroyed. - HWND GetHandle(); - - // If true, closing this window will quit the application. - void SetQuitOnClose(bool quit_on_close); - - // Return a RECT representing the bounds of the current client area. - RECT GetClientArea(); - - protected: - // Processes and route salient window messages for mouse handling, - // size change and DPI. Delegates handling of these to member overloads that - // inheriting classes can handle. - virtual LRESULT MessageHandler(HWND window, - UINT const message, - WPARAM const wparam, - LPARAM const lparam) noexcept; - - // Called when CreateAndShow is called, allowing subclass window-related - // setup. Subclasses should return false if setup fails. - virtual bool OnCreate(); - - // Called when Destroy is called. - virtual void OnDestroy(); - - private: - friend class WindowClassRegistrar; - - // OS callback called by message pump. Handles the WM_NCCREATE message which - // is passed when the non-client area is being created and enables automatic - // non-client DPI scaling so that the non-client area automatically - // responsponds to changes in DPI. All other messages are handled by - // MessageHandler. - static LRESULT CALLBACK WndProc(HWND const window, - UINT const message, - WPARAM const wparam, - LPARAM const lparam) noexcept; - - // Retrieves a class instance pointer for |window| - static Win32Window* GetThisFromHandle(HWND const window) noexcept; - - bool quit_on_close_ = false; - - // window handle for top level window. - HWND window_handle_ = nullptr; - - // window handle for hosted content. - HWND child_content_ = nullptr; -}; - -#endif // RUNNER_WIN32_WINDOW_H_ +#ifndef RUNNER_WIN32_WINDOW_H_ +#define RUNNER_WIN32_WINDOW_H_ + +#include + +#include +#include +#include + +// A class abstraction for a high DPI-aware Win32 Window. Intended to be +// inherited from by classes that wish to specialize with custom +// rendering and input handling +class Win32Window { + public: + struct Point { + unsigned int x; + unsigned int y; + Point(unsigned int x, unsigned int y) : x(x), y(y) {} + }; + + struct Size { + unsigned int width; + unsigned int height; + Size(unsigned int width, unsigned int height) + : width(width), height(height) {} + }; + + Win32Window(); + virtual ~Win32Window(); + + // Creates a win32 window with |title| that is positioned and sized using + // |origin| and |size|. New windows are created on the default monitor. Window + // sizes are specified to the OS in physical pixels, hence to ensure a + // consistent size this function will scale the inputted width and height as + // as appropriate for the default monitor. The window is invisible until + // |Show| is called. Returns true if the window was created successfully. + bool Create(const std::wstring& title, const Point& origin, const Size& size); + + // Show the current window. Returns true if the window was successfully shown. + bool Show(); + + // Release OS resources associated with window. + void Destroy(); + + // Inserts |content| into the window tree. + void SetChildContent(HWND content); + + // Returns the backing Window handle to enable clients to set icon and other + // window properties. Returns nullptr if the window has been destroyed. + HWND GetHandle(); + + // If true, closing this window will quit the application. + void SetQuitOnClose(bool quit_on_close); + + // Return a RECT representing the bounds of the current client area. + RECT GetClientArea(); + + protected: + // Processes and route salient window messages for mouse handling, + // size change and DPI. Delegates handling of these to member overloads that + // inheriting classes can handle. + virtual LRESULT MessageHandler(HWND window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Called when CreateAndShow is called, allowing subclass window-related + // setup. Subclasses should return false if setup fails. + virtual bool OnCreate(); + + // Called when Destroy is called. + virtual void OnDestroy(); + + private: + friend class WindowClassRegistrar; + + // OS callback called by message pump. Handles the WM_NCCREATE message which + // is passed when the non-client area is being created and enables automatic + // non-client DPI scaling so that the non-client area automatically + // responds to changes in DPI. All other messages are handled by + // MessageHandler. + static LRESULT CALLBACK WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Retrieves a class instance pointer for |window| + static Win32Window* GetThisFromHandle(HWND const window) noexcept; + + // Update the window frame's theme to match the system theme. + static void UpdateTheme(HWND const window); + + bool quit_on_close_ = false; + + // window handle for top level window. + HWND window_handle_ = nullptr; + + // window handle for hosted content. + HWND child_content_ = nullptr; +}; + +#endif // RUNNER_WIN32_WINDOW_H_ diff --git a/flutter_quill_extensions/.pubignore b/flutter_quill_extensions/.pubignore new file mode 100644 index 00000000..757d78e3 --- /dev/null +++ b/flutter_quill_extensions/.pubignore @@ -0,0 +1,3 @@ +# For local development +pubspec_overrides.yaml +pubspec_overrides.yaml.disabled \ No newline at end of file diff --git a/flutter_quill_extensions/CHANGELOG.md b/flutter_quill_extensions/CHANGELOG.md index 67c40380..a1a9dc3e 100644 --- a/flutter_quill_extensions/CHANGELOG.md +++ b/flutter_quill_extensions/CHANGELOG.md @@ -1,61 +1,136 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +## 0.6.10 +* Update deprecated members from `flutter_quill` +* Update doc and `README.md` + +## 0.6.9 +* Remove duplicated class +* Drop the support for `QuillEditorFormulaEmbedBuilder` for now as it's not usable, we are working on providing a fixes +* Fix bug with the zoom button + +## 0.6.8 +* Feature: Allow the developer to override the `assetsPrefix` and default value is `assets`, you should define this correctly if you planning on using asset images in the `QuillEditor`, take a look at `QuillSharedExtensionsConfigurations` class for more info + +## 0.6.7 +* Support the new localization system of `flutter_quill` + +## 0.6.6 +* Add `onImageClicked` in the `QuillEditorImageEmbedConfigurations` +* Fix image resizing on mobile + +## 0.6.5 +* Support the new improved platform checking of `flutter_quill` +* Update the Image embed builder logic +* Fix the Save image button exception +* Feature: Image cropping for the image embed builder +* Add support for copying the image to the clipboard +* Add a new static method in `FlutterQuillEmbeds` which is `defaultEditorBuilders` for minimal configurations +* Fix the image size logic (it's still missing a lot of things but we will work on that soon) +* Fix the zoom image functionality to support different image providers +* Fix the typo in the function name `editorsWebBuilders`, now it's called `editorWebBuilders` +* Deprecated: The boolean property `forceUseMobileOptionMenuForImageClick` is now deprecated as we will not using it anymore and it will be removed in the next major release +* Update `README.md` + +## 0.6.4 +* Update `QuillImageUtilities` +* Add new extension on `QuillController` to access `QuillImageUtilities` instance easier +* Support the new `iconButtonFactor` property + +## 0.6.3 +* Update `README.md` + +## 0.6.2 +* Add more default exports + +## 0.6.1 +* Fix bug on web that causing the project to not build + +## 0.6.0 +* This version is not stable yet as it doesn't have mirgration guide and missing some things and we might introduce more breaking changes very soon but we decided to publish it because the latest stable version is not compatible with latest stable version of `flutter_quill` + +## 0.6.0-dev.6 +* Better support for web +* Smal fixes and updates + +## 0.6.0-dev.5 +* Update the camera button + +## 0.6.0-dev.4 +* Add more exports +* Update `README.md`` +* Fix save image bug +* Quick fixes + +## 0.6.0-dev.3 +* Disable the camera option by default on desktop + +## 0.6.0-dev.2 +* Another breaking changes, we will add mirgrate guide soon. + +## 0.6.0-dev.1 +* Breaking Changes, we have refactored most of the functions and it got renamed + ## 0.5.1 -- Provide a way to use custom image provider for the image widgets -- Provide a way to handle different errors in image widgets -- Two bug fixes related to pick the image and capture it using the camera -- Add support for image resizing on desktop platforms when forced using the mobile context menu -- Improve performance by reducing the number of widgets rebuilt by listening to media query for only the needed things, for example instead of using `MediaQuery.of(context).size`, now we are using `MediaQuery.sizeOf(context)` -- Fix warrning "The platformViewRegistry getter is deprecated and will be removed in a future release. Please import it from dart:ui_web instead." -- Add QuillImageUtilities class -- Small improvemenets -- Allow to use the mobile context menu on desktop by force using it -- Add the resizing option to the forced mobile context menu -- Add new custom style attrbuite for desktop and other platforms +* Provide a way to use custom image provider for the image widgets +* Provide a way to handle different errors in image widgets +* Two bug fixes related to pick the image and capture it using the camera +* Add support for image resizing on desktop platforms when forced using the mobile context menu +* Improve performance by reducing the number of widgets rebuilt by listening to media query for only the needed things, for example instead of using `MediaQuery.of(context).size`, now we are using `MediaQuery.sizeOf(context)` +* Fix warrning "The platformViewRegistry getter is deprecated and will be removed in a future release. Please import it from dart:ui_web instead." +* Add QuillImageUtilities class +* Small improvemenets +* Allow to use the mobile context menu on desktop by force using it +* Add the resizing option to the forced mobile context menu +* Add new custom style attrbuite for desktop and other platforms ## 0.5.0 -- Migrated from `gallery_saver` to `gal` for saving images -- Added callbacks for greater control of editing images +* Migrated from `gallery_saver` to `gal` for saving images +* Added callbacks for greater control of editing images ## 0.4.1 -- Updated dependencies to support image_picker 1.0 +* Updated dependencies to support image_picker 1.0 ## 0.4.0 -- Fix backspace around images [PR #1309](https://github.com/singerdmx/flutter-quill/pull/1309) -- Feat/link regexp [PR #1329](https://github.com/singerdmx/flutter-quill/pull/1329) +* Fix backspace around images [PR #1309](https://github.com/singerdmx/flutter-quill/pull/1309) +* Feat/link regexp [PR #1329](https://github.com/singerdmx/flutter-quill/pull/1329) ## 0.3.4 -- Resolve deprecated method use in the `video_player` package +* Resolve deprecated method use in the `video_player` package ## 0.3.3 -- Fix a prototype bug which was bring by [PR #1230](https://github.com/singerdmx/flutter-quill/pull/1230#issuecomment-1560597099) +* Fix a prototype bug which was bring by [PR #1230](https://github.com/singerdmx/flutter-quill/pull/1230#issuecomment*1560597099) ## 0.3.2 -- Updated dependencies to support intl 0.18 +* Updated dependencies to support intl 0.18 ## 0.3.1 -- Image embedding tweaks - - Add MediaButton which is intened to superseed the ImageButton and VideoButton. Only image selection is working. - - Implement image insert for web (image as base64) +* Image embedding tweaks + * Add MediaButton which is intened to superseed the ImageButton and VideoButton. Only image selection is working. + * Implement image insert for web (image as base64) ## 0.3.0 -- Added support for adding custom tooltips to toolbar buttons +* Added support for adding custom tooltips to toolbar buttons ## 0.2.0 -- Allow widgets to override widget span properties [b7951b0](https://github.com/singerdmx/flutter-quill/commit/b7951b02c9086ea42e7aad6d78e6c9b0297562e5) -- Remove tuples [3e9452e](https://github.com/singerdmx/flutter-quill/commit/3e9452e675e8734ff50364c5f7b5d34088d5ff05) -- Remove transparent color of ImageVideoUtils dialog [74544bd](https://github.com/singerdmx/flutter-quill/commit/74544bd945a9d212ca1e8d6b3053dbecee22b720) -- Migrate to `youtube_player_flutter` from `youtube_player_flutter_quill` -- Updates to forumla button [5228f38](https://github.com/singerdmx/flutter-quill/commit/5228f389ba6f37d61d445cfe138c19fcf8766d71) +* Allow widgets to override widget span properties [b7951b0](https://github.com/singerdmx/flutter-quill/commit/b7951b02c9086ea42e7aad6d78e6c9b0297562e5) +* Remove tuples [3e9452e](https://github.com/singerdmx/flutter-quill/commit/3e9452e675e8734ff50364c5f7b5d34088d5ff05) +* Remove transparent color of ImageVideoUtils dialog [74544bd](https://github.com/singerdmx/flutter-quill/commit/74544bd945a9d212ca1e8d6b3053dbecee22b720) +* Migrate to `youtube_player_flutter` from `youtube_player_flutter_quill` +* Updates to forumla button [5228f38](https://github.com/singerdmx/flutter-quill/commit/5228f389ba6f37d61d445cfe138c19fcf8766d71) ## 0.1.0 -- Initial release +* Initial release diff --git a/flutter_quill_extensions/LICENSE b/flutter_quill_extensions/LICENSE index 498b3e0a..e82b91ed 100644 --- a/flutter_quill_extensions/LICENSE +++ b/flutter_quill_extensions/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2020 Xin Yao +Copyright (c) 2023 Flutter Quill Team Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/flutter_quill_extensions/README.md b/flutter_quill_extensions/README.md index a685b8f0..af85514f 100644 --- a/flutter_quill_extensions/README.md +++ b/flutter_quill_extensions/README.md @@ -1,22 +1,258 @@ # Flutter Quill Extensions -Helpers to support embed widgets in flutter_quill. See [Flutter Quill](https://pub.dev/packages/flutter_quill) for details of use. +An extensions for [flutter_quill](https://pub.dev/packages/flutter_quill) +to support embed widgets like image, formula, video and more. + + Check [Flutter Quill](https://github.com/singerdmx/flutter-quill) for details of use. + + ## Table of Contents + +- [Flutter Quill Extensions](#flutter-quill-extensions) + - [Table of Contents](#table-of-contents) + - [About](#about) + - [Installation](#installation) + - [Platform Spesefic Configurations](#platform-spesefic-configurations) + - [Usage](#usage) + - [Embed Blocks](#embed-blocks) + - [Custom Size Image for Mobile](#custom-size-image-for-mobile) + - [Custom Size Image for other platforms](#custom-size-image-for-other-platforms) + - [Drag and drop feature](#drag-and-drop-feature) + - [Features](#features) + - [Contributing](#contributing) + - [Acknowledgments](#acknowledgments) + + +## About + +Flutter quill is a rich editor text. It'd allow you to customize a lot of things, +it has custom embed builders which allow you to render custom widgets in the editor
+this is an extension to extend its functionalities by adding more features like images, videos, and more + +## Installation + +Before starting using this package, please make sure to install +[flutter_quill](https://github.com/singerdmx/flutter-quill) package first and follow +its usage instructions. + +```yaml +dependencies: + flutter_quill_extensions: ^ +``` + +

OR

+ +```yaml +dependencies: + flutter_quill_extensions: + git: https://github.com/singerdmx/flutter-quill.git + path: flutter_quill_extensions +``` + +## Platform Spesefic Configurations + +> +> 1. We are using [`gal`](https://github.com/natsuk4ze/) plugin to save images. +> For this to work, you need to add the appropriate permissions +> to your `Info.plist` and `AndroidManifest.xml` files. +> See to add the needed lines. +> +> 2. We also use [`image_picker`](https://pub.dev/packages/image_picker) plugin for picking images so please make sure follow the instructions +> +> 3. For loading the image from the internet, we need internet permission +> 1. For Android, you need to add some permissions in `AndroidManifest.xml`, Please follow this [link](https://developer.android.com/training/basics/network-ops/connecting) for more info, the internet permission included by default only for debugging so you need to follow this link to add it in the release version too. you should allow loading images and videos only for the `https` protocol but if you want http too then you need to configure your android application to accept `http` in the release mode, follow this [link](https://stackoverflow.com/questions/45940861/android-8-cleartext-http-traffic-not-permitted) for more info. +> 2. for macOS, you also need to include a key in your `Info.plist`, please follow this [link](https://stackoverflow.com/a/61201081/18519412) to add the required configurations +> +> The extension package also uses [image_picker](https://pub.dev/packages/image_picker) which also +> requires some configurations, follow this [link](https://pub.dev/packages/image_picker#installation). It's needed for Android, iOS, macOS, we must inform you that you can't pick photo using camera in desktop so make sure to handle that if you plan on add support for desktop, this may change in the future and for more info follow this [link](https://pub.dev/packages/image_picker#windows-macos-and-linux)
+> ## Usage -Set the `embedBuilders` and `embedToolbar` params in `QuillEditor` and `QuillToolbar` with the +Before starting using this package you must follow the [setup](#installation) + +Set the `embedBuilders` and `embedToolbar` params in configurations of `QuillEditor` and `QuillToolbar` with the values provided by this repository. +**Quill Toolbar**: +```dart +QuillToolbar( + configurations: QuillToolbarConfigurations( + embedButtons: FlutterQuillEmbeds.toolbarButtons(), + ), +), +``` + +**Quill Editor** +```dart +Expanded( + child: QuillEditor.basic( + configurations: QuillEditorConfigurations( + embedBuilders: kIsWeb ? FlutterQuillEmbeds.editorsWebBuilders() : FlutterQuillEmbeds.editorBuilders(), + ), + ), +) +``` + +They both should be have a parent `QuillProvider` in the widget tree and setup properly
+Example: + +```dart +QuillProvider( + configurations: QuillConfigurations( + controller: _controller, + sharedConfigurations: const QuillSharedConfigurations(), + ), + child: Column( + children: [ + QuillToolbar( + configurations: QuillToolbarConfigurations( + embedButtons: FlutterQuillEmbeds.toolbarButtons( + imageButtonOptions: QuillToolbarImageButtonOptions(), + ), + ), + ), + Expanded( + child: QuillEditor.basic( + configurations: QuillEditorConfigurations( + padding: const EdgeInsets.all(16), + embedBuilders: kIsWeb ? FlutterQuillEmbeds.editorsWebBuilders() : FlutterQuillEmbeds.editorBuilders(), + ), + ), + ) + ], + ), +) +``` + +## Embed Blocks + +As of version [flutter_quill](https://pub.dev/packages/flutter_quill) 6.0, embed blocks are not provided by default as part of Flutter quill. Instead, it provides an interface to all the user to provide there own implementations for embed blocks. Implementations for image, video and formula embed blocks is proved in this package + +The instructions for using the embed blocks is in the [Usage](#usage) section + +### Custom Size Image for Mobile + +Define `mobileWidth`, `mobileHeight`, `mobileMargin`, `mobileAlignment` as follows: + +```json +{ + "insert": { + "image": "https://user-images.githubusercontent.com/122956/72955931-ccc07900-3d52-11ea-89b1-d468a6e2aa2b.png" + }, + "attributes":{ + "style":"mobileWidth: 50; mobileHeight: 50; mobileMargin: 10; mobileAlignment: topLeft" + } +} ``` -QuillEditor.basic( - controller: controller, - embedBuilders: FlutterQuillEmbeds.builders(), -); + +### Custom Size Image for other platforms + +Define `width`, `height`, `margin`, `alignment` as follows: + +```json +{ + "insert": { + "image": "https://user-images.githubusercontent.com/122956/72955931-ccc07900-3d52-11ea-89b1-d468a6e2aa2b.png" + }, + "attributes": { + "style":"width: 50; height: 50; margin: 10; alignment: topLeft" + } +} ``` + + +### Drag and drop feature +Currently, the drag and drop feature is not officially supported, but you can achieve this very easily in the following steps: + +1. Drag and drop require native code, you can use any flutter plugin you like, if you want a suggestion we recommend [desktop_drop](https://pub.dev/packages/desktop_drop), it was origanlly developed for desktop but it has support for web as well mobile platforms +2. Add the dependency in your `pubspec.yaml` using the following command: + + ```yaml + flutter pub add desktop_drop + ``` + and import it with + ```dart + import 'package:desktop_drop/desktop_drop.dart'; + ``` +3. in the configurations of `QuillEditor`, use the `builder` to wrap the editor with `DropTarget` which comes from `desktop_drop` + + ```dart + import 'package:flutter_quill_extensions/flutter_quill_extensions.dart'; + + QuillEditor.basic( + configurations: QuillEditorConfigurations( + padding: const EdgeInsets.all(16), + builder: (context, rawEditor) { + return DropTarget( + onDragDone: _onDragDone, + child: rawEditor, + ); + }, + embedBuilders: kIsWeb + ? FlutterQuillEmbeds.editorsWebBuilders() + : FlutterQuillEmbeds.editorBuilders(), + ), + ) + ``` +4. Implement the `_onDragDone`, it depends on your use case but this is just a simple example +```dart +const List imageFileExtensions = [ + '.jpeg', + '.png', + '.jpg', + '.gif', + '.webp', + '.tif', + '.heic' +]; +OnDragDoneCallback get _onDragDone { + return (details) { + final scaffoldMessenger = ScaffoldMessenger.of(context); + final file = details.files.first; + final isSupported = + imageFileExtensions.any((ext) => file.name.endsWith(ext)); + if (!isSupported) { + scaffoldMessenger.showSnackBar( + SnackBar( + content: Text( + 'Only images are supported right now: ${file.mimeType}, ${file.name}, ${file.path}, $imageFileExtensions', + ), + ), + ); + return; + } + // To get this extension function please import flutter_quill_extensions + _controller.insertImageBlock( + imageSource: file.path, + ); + scaffoldMessenger.showSnackBar( + const SnackBar( + content: Text('Image is inserted.'), + ), + ); + }; + } ``` -QuillToolbar.basic( - controller: controller, - embedButtons: FlutterQuillEmbeds.buttons(), -); + +## Features + +```markdown +## Features + +— Easy to use and customizable +- Has the option to use custom image provider for the images +- Useful utilities and widgets +- Handle different errors ``` + +## Contributing + +We welcome contributions! + +Please follow these guidelines when contributing to our project. See [CONTRIBUTING.md](../CONTRIBUTING.md) for more details. + +## Acknowledgments + +- Thanks to the [Flutter Team](https://flutter.dev/) +- Thanks to [flutter_quill](https://pub.dev/packages/flutter_quill) diff --git a/flutter_quill_extensions/analysis_options.yaml b/flutter_quill_extensions/analysis_options.yaml index 7749c861..f1a38172 100644 --- a/flutter_quill_extensions/analysis_options.yaml +++ b/flutter_quill_extensions/analysis_options.yaml @@ -1,4 +1,4 @@ -include: package:pedantic/analysis_options.yaml +include: package:flutter_lints/flutter.yaml analyzer: errors: @@ -6,32 +6,31 @@ analyzer: unsafe_html: ignore linter: rules: - - always_declare_return_types - - always_put_required_named_parameters_first - - annotate_overrides - - avoid_empty_else - - avoid_escaping_inner_quotes - - avoid_print - - avoid_redundant_argument_values - - avoid_types_on_closure_parameters - - avoid_void_async - - cascade_invocations - - directives_ordering - - lines_longer_than_80_chars - - omit_local_variable_types - - prefer_const_constructors - - prefer_const_constructors_in_immutables - - prefer_const_declarations - - prefer_final_fields - - prefer_final_in_for_each - - prefer_final_locals - - prefer_initializing_formals - - prefer_int_literals - - prefer_interpolation_to_compose_strings - - prefer_relative_imports - - prefer_single_quotes - - sort_constructors_first - - sort_unnamed_constructors_first - - unnecessary_lambdas - - unnecessary_parenthesis - - unnecessary_string_interpolations + 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: true + avoid_redundant_argument_values: true + 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 diff --git a/flutter_quill_extensions/lib/embeds/builders.dart b/flutter_quill_extensions/lib/embeds/builders.dart deleted file mode 100644 index 2492fd7c..00000000 --- a/flutter_quill_extensions/lib/embeds/builders.dart +++ /dev/null @@ -1,476 +0,0 @@ -import 'dart:io' show File; - -import 'package:flutter/cupertino.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_quill/extensions.dart' as base; -import 'package:flutter_quill/flutter_quill.dart'; -import 'package:flutter_quill/translations.dart'; -import 'package:math_keyboard/math_keyboard.dart'; -import 'package:universal_html/html.dart' as html; - -import '../shims/dart_ui_fake.dart' - if (dart.library.html) '../shims/dart_ui_real.dart' as ui; -import 'embed_types.dart'; -import 'utils.dart'; -import 'widgets/image.dart'; -import 'widgets/image_resizer.dart'; -import 'widgets/video_app.dart'; -import 'widgets/youtube_video_app.dart'; - -class ImageEmbedBuilder extends EmbedBuilder { - ImageEmbedBuilder({ - required this.imageProviderBuilder, - required this.imageErrorWidgetBuilder, - required this.onImageRemovedCallback, - required this.shouldRemoveImageCallback, - this.forceUseMobileOptionMenu = false, - }); - final ImageEmbedBuilderOnRemovedCallback? onImageRemovedCallback; - final ImageEmbedBuilderWillRemoveCallback? shouldRemoveImageCallback; - final bool forceUseMobileOptionMenu; - final ImageEmbedBuilderProviderBuilder? imageProviderBuilder; - final ImageEmbedBuilderErrorWidgetBuilder? imageErrorWidgetBuilder; - - @override - String get key => BlockEmbed.imageType; - - @override - bool get expanded => false; - - @override - Widget build( - BuildContext context, - QuillController controller, - base.Embed node, - bool readOnly, - bool inline, - TextStyle textStyle, - ) { - assert(!kIsWeb, 'Please provide image EmbedBuilder for Web'); - - Widget image = const SizedBox.shrink(); - final imageUrl = standardizeImageUrl(node.value.data); - OptionalSize? imageSize; - final style = node.style.attributes['style']; - - if (style != null) { - final attrs = base.isMobile() - ? base.parseKeyValuePairs(style.value.toString(), { - Attribute.mobileWidth, - Attribute.mobileHeight, - Attribute.mobileMargin, - Attribute.mobileAlignment, - }) - : base.parseKeyValuePairs(style.value.toString(), { - Attribute.width.key, - Attribute.height.key, - Attribute.margin, - Attribute.alignment, - }); - if (attrs.isNotEmpty) { - final width = double.tryParse( - (base.isMobile() - ? attrs[Attribute.mobileWidth] - : attrs[Attribute.width.key]) ?? - '', - ); - final height = double.tryParse( - (base.isMobile() - ? attrs[Attribute.mobileHeight] - : attrs[Attribute.height.key]) ?? - '', - ); - final alignment = base.getAlignment(base.isMobile() - ? attrs[Attribute.mobileAlignment] - : attrs[Attribute.alignment]); - final margin = (base.isMobile() - ? double.tryParse(Attribute.mobileMargin) - : double.tryParse(Attribute.margin)) ?? - 0.0; - - assert( - width != null && height != null, - base.isMobile() - ? 'mobileWidth and mobileHeight must be specified' - : 'width and height must be specified', - ); - imageSize = OptionalSize(width, height); - image = Padding( - padding: EdgeInsets.all(margin), - child: getQuillImageByUrl( - imageUrl, - width: width, - height: height, - alignment: alignment, - imageProviderBuilder: imageProviderBuilder, - imageErrorWidgetBuilder: imageErrorWidgetBuilder, - ), - ); - } - } - - if (imageSize == null) { - image = getQuillImageByUrl( - imageUrl, - imageProviderBuilder: imageProviderBuilder, - imageErrorWidgetBuilder: imageErrorWidgetBuilder, - ); - imageSize = OptionalSize((image as Image).width, image.height); - } - - if (!readOnly && (base.isMobile() || forceUseMobileOptionMenu)) { - return GestureDetector( - onTap: () { - showDialog( - context: context, - builder: (context) { - final copyOption = _SimpleDialogItem( - icon: Icons.copy_all_outlined, - color: Colors.cyanAccent, - text: 'Copy'.i18n, - onPressed: () { - final imageNode = - getEmbedNode(controller, controller.selection.start) - .value; - final imageUrl = imageNode.value.data; - controller.copiedImageUrl = - ImageUrl(imageUrl, getImageStyleString(controller)); - Navigator.pop(context); - }, - ); - final removeOption = _SimpleDialogItem( - icon: Icons.delete_forever_outlined, - color: Colors.red.shade200, - text: 'Remove'.i18n, - onPressed: () async { - Navigator.of(context).pop(); - - final imageFile = File(imageUrl); - - // Call the remove check callback if set - if (await shouldRemoveImageCallback?.call(imageFile) == - false) { - return; - } - - final offset = getEmbedNode( - controller, - controller.selection.start, - ).offset; - controller.replaceText( - offset, - 1, - '', - TextSelection.collapsed(offset: offset), - ); - // Call the post remove callback if set - await onImageRemovedCallback?.call(imageFile); - }, - ); - return Padding( - padding: const EdgeInsets.fromLTRB(50, 0, 50, 0), - child: SimpleDialog( - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.all( - Radius.circular(10), - ), - ), - children: [ - _SimpleDialogItem( - icon: Icons.settings_outlined, - color: Colors.lightBlueAccent, - text: 'Resize'.i18n, - onPressed: () { - Navigator.pop(context); - showCupertinoModalPopup( - context: context, - builder: (context) { - final screenSize = MediaQuery.sizeOf(context); - return ImageResizer( - onImageResize: (w, h) { - final res = getEmbedNode( - controller, - controller.selection.start, - ); - - final attr = - base.replaceStyleStringWithSize( - getImageStyleString(controller), - width: w, - height: h, - isMobile: base.isMobile(), - ); - controller - ..skipRequestKeyboard = true - ..formatText( - res.offset, - 1, - StyleAttribute(attr), - ); - }, - imageWidth: imageSize?.width, - imageHeight: imageSize?.height, - maxWidth: screenSize.width, - maxHeight: screenSize.height, - ); - }, - ); - }, - ), - copyOption, - removeOption, - ]), - ); - }); - }, - child: image, - ); - } - - if (!readOnly || isImageBase64(imageUrl)) { - // To enforce using it on the web, desktop and other platforms - // and that is up to the developer - if (!base.isMobile() && forceUseMobileOptionMenu) { - return _menuOptionsForReadonlyImage( - context: context, - imageUrl: imageUrl, - image: image, - imageProviderBuilder: imageProviderBuilder, - imageErrorWidgetBuilder: imageErrorWidgetBuilder, - ); - } - return image; - } - - // We provide option menu for mobile platform excluding base64 image - return _menuOptionsForReadonlyImage( - context: context, - imageUrl: imageUrl, - image: image, - imageProviderBuilder: imageProviderBuilder, - imageErrorWidgetBuilder: imageErrorWidgetBuilder, - ); - } -} - -class ImageEmbedBuilderWeb extends EmbedBuilder { - ImageEmbedBuilderWeb({this.constraints}) - : assert(kIsWeb, 'ImageEmbedBuilderWeb is only for web platform'); - - final BoxConstraints? constraints; - - @override - String get key => BlockEmbed.imageType; - - @override - Widget build( - BuildContext context, - QuillController controller, - Embed node, - bool readOnly, - bool inline, - TextStyle textStyle, - ) { - final imageUrl = node.value.data; - - ui.platformViewRegistry.registerViewFactory(imageUrl, (viewId) { - return html.ImageElement() - ..src = imageUrl - ..style.height = 'auto' - ..style.width = 'auto'; - }); - - return ConstrainedBox( - constraints: constraints ?? BoxConstraints.loose(const Size(200, 200)), - child: HtmlElementView( - viewType: imageUrl, - ), - ); - } -} - -class VideoEmbedBuilder extends EmbedBuilder { - VideoEmbedBuilder({this.onVideoInit}); - - final void Function(GlobalKey videoContainerKey)? onVideoInit; - - @override - String get key => BlockEmbed.videoType; - - @override - Widget build( - BuildContext context, - QuillController controller, - base.Embed node, - bool readOnly, - bool inline, - TextStyle textStyle, - ) { - assert(!kIsWeb, 'Please provide video EmbedBuilder for Web'); - - final videoUrl = node.value.data; - if (isYouTubeUrl(videoUrl)) { - return YoutubeVideoApp( - videoUrl: videoUrl, context: context, readOnly: readOnly); - } - return VideoApp( - videoUrl: videoUrl, - context: context, - readOnly: readOnly, - onVideoInit: onVideoInit, - ); - } -} - -class FormulaEmbedBuilder extends EmbedBuilder { - @override - String get key => BlockEmbed.formulaType; - - @override - Widget build( - BuildContext context, - QuillController controller, - base.Embed node, - bool readOnly, - bool inline, - TextStyle textStyle, - ) { - assert(!kIsWeb, 'Please provide formula EmbedBuilder for Web'); - - final mathController = MathFieldEditingController(); - return Focus( - onFocusChange: (hasFocus) { - if (hasFocus) { - // If the MathField is tapped, hides the built in keyboard - SystemChannels.textInput.invokeMethod('TextInput.hide'); - debugPrint(mathController.currentEditingValue()); - } - }, - child: MathField( - controller: mathController, - variables: const ['x', 'y', 'z'], - onChanged: (value) {}, - onSubmitted: (value) {}, - ), - ); - } -} - -Widget _menuOptionsForReadonlyImage({ - required BuildContext context, - required String imageUrl, - required Widget image, - required ImageEmbedBuilderProviderBuilder? imageProviderBuilder, - required ImageEmbedBuilderErrorWidgetBuilder? imageErrorWidgetBuilder, -}) { - return GestureDetector( - onTap: () { - showDialog( - context: context, - builder: (context) { - final saveOption = _SimpleDialogItem( - icon: Icons.save, - color: Colors.greenAccent, - text: 'Save'.i18n, - onPressed: () async { - imageUrl = appendFileExtensionToImageUrl(imageUrl); - final messenger = ScaffoldMessenger.of(context); - Navigator.of(context).pop(); - - final saveImageResult = await saveImage(imageUrl); - final imageSavedSuccessfully = saveImageResult.isSuccess; - - messenger.clearSnackBars(); - - if (!imageSavedSuccessfully) { - messenger.showSnackBar(SnackBar( - content: Text( - 'Error while saving image'.i18n, - ))); - return; - } - - var message; - switch (saveImageResult.method) { - case SaveImageResultMethod.network: - message = 'Saved using the network'.i18n; - break; - case SaveImageResultMethod.localStorage: - message = 'Saved using the local storage'.i18n; - break; - } - - messenger.showSnackBar( - SnackBar( - content: Text(message), - ), - ); - }, - ); - final zoomOption = _SimpleDialogItem( - icon: Icons.zoom_in, - color: Colors.cyanAccent, - text: 'Zoom'.i18n, - onPressed: () { - Navigator.pushReplacement( - context, - MaterialPageRoute( - builder: (context) => ImageTapWrapper( - imageUrl: imageUrl, - imageProviderBuilder: imageProviderBuilder, - imageErrorWidgetBuilder: imageErrorWidgetBuilder, - ), - ), - ); - }, - ); - return Padding( - padding: const EdgeInsets.fromLTRB(50, 0, 50, 0), - child: SimpleDialog( - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.all( - Radius.circular(10), - ), - ), - children: [saveOption, zoomOption], - ), - ); - }, - ); - }, - child: image); -} - -class _SimpleDialogItem extends StatelessWidget { - const _SimpleDialogItem( - {required this.icon, - required this.color, - required this.text, - required this.onPressed, - Key? key}) - : super(key: key); - - final IconData icon; - final Color color; - final String text; - final VoidCallback onPressed; - - @override - Widget build(BuildContext context) { - return SimpleDialogOption( - onPressed: onPressed, - child: Row( - children: [ - Icon(icon, size: 36, color: color), - Padding( - padding: const EdgeInsetsDirectional.only(start: 16), - child: - Text(text, style: const TextStyle(fontWeight: FontWeight.bold)), - ), - ], - ), - ); - } -} diff --git a/flutter_quill_extensions/lib/embeds/embed_types.dart b/flutter_quill_extensions/lib/embeds/embed_types.dart deleted file mode 100644 index 9fa4184a..00000000 --- a/flutter_quill_extensions/lib/embeds/embed_types.dart +++ /dev/null @@ -1,62 +0,0 @@ -import 'dart:io' show File; -import 'dart:typed_data'; - -import 'package:flutter/material.dart' - show ImageErrorWidgetBuilder, BuildContext, ImageProvider; - -typedef OnImagePickCallback = Future Function(File file); -typedef OnVideoPickCallback = Future Function(File file); -typedef FilePickImpl = Future Function(BuildContext context); -typedef WebImagePickImpl = Future Function( - OnImagePickCallback onImagePickCallback); -typedef WebVideoPickImpl = Future Function( - OnVideoPickCallback onImagePickCallback); -typedef MediaPickSettingSelector = Future Function( - BuildContext context); - -enum MediaPickSetting { - Gallery, - Link, - Camera, - Video, -} - -typedef MediaFileUrl = String; -typedef MediaFilePicker = Future Function(QuillMediaType mediaType); -typedef MediaPickedCallback = Future Function(QuillFile file); - -enum QuillMediaType { image, video } - -extension QuillMediaTypeX on QuillMediaType { - bool get isImage => this == QuillMediaType.image; - bool get isVideo => this == QuillMediaType.video; -} - -/// Represents a file data which returned by file picker. -class QuillFile { - QuillFile({ - required this.name, - this.path = '', - Uint8List? bytes, - }) : assert(name.isNotEmpty), - bytes = bytes ?? Uint8List(0); - - final String name; - final String path; - final Uint8List bytes; -} - -typedef ImageEmbedBuilderWillRemoveCallback = Future Function( - File imageFile, -); - -typedef ImageEmbedBuilderOnRemovedCallback = Future Function( - File imageFile, -); - -typedef ImageEmbedBuilderProviderBuilder = ImageProvider Function( - String imageUrl, - // {required bool isLocalImage} -); - -typedef ImageEmbedBuilderErrorWidgetBuilder = ImageErrorWidgetBuilder; diff --git a/flutter_quill_extensions/lib/embeds/toolbar/camera_button.dart b/flutter_quill_extensions/lib/embeds/toolbar/camera_button.dart deleted file mode 100644 index 2c512567..00000000 --- a/flutter_quill_extensions/lib/embeds/toolbar/camera_button.dart +++ /dev/null @@ -1,159 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_quill/flutter_quill.dart'; -import 'package:flutter_quill/translations.dart'; -import 'package:image_picker/image_picker.dart'; - -import '../embed_types.dart'; -import 'image_video_utils.dart'; - -class CameraButton extends StatelessWidget { - const CameraButton({ - required this.icon, - required this.controller, - this.iconSize = kDefaultIconSize, - this.fillColor, - this.onImagePickCallback, - this.onVideoPickCallback, - this.filePickImpl, - this.webImagePickImpl, - this.webVideoPickImpl, - this.cameraPickSettingSelector, - this.iconTheme, - this.tooltip, - Key? key, - }) : super(key: key); - - final IconData icon; - final double iconSize; - - final Color? fillColor; - - final QuillController controller; - - final OnImagePickCallback? onImagePickCallback; - - final OnVideoPickCallback? onVideoPickCallback; - - final WebImagePickImpl? webImagePickImpl; - - final WebVideoPickImpl? webVideoPickImpl; - - final FilePickImpl? filePickImpl; - - final MediaPickSettingSelector? cameraPickSettingSelector; - - final QuillIconTheme? iconTheme; - final String? tooltip; - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - - final iconColor = iconTheme?.iconUnselectedColor ?? theme.iconTheme.color; - final iconFillColor = - iconTheme?.iconUnselectedFillColor ?? (fillColor ?? theme.canvasColor); - - return QuillToolbarIconButton( - icon: Icon(icon, size: iconSize, color: iconColor), - tooltip: tooltip, - highlightElevation: 0, - hoverElevation: 0, - size: iconSize * 1.77, - fillColor: iconFillColor, - borderRadius: iconTheme?.borderRadius ?? 2, - onPressed: () => _handleCameraButtonTap( - context, - controller, - onImagePickCallback: onImagePickCallback, - onVideoPickCallback: onVideoPickCallback, - filePickImpl: filePickImpl, - webImagePickImpl: webImagePickImpl, - ), - ); - } - - Future _handleCameraButtonTap( - BuildContext context, - QuillController controller, { - OnImagePickCallback? onImagePickCallback, - OnVideoPickCallback? onVideoPickCallback, - FilePickImpl? filePickImpl, - WebImagePickImpl? webImagePickImpl, - }) async { - if (onVideoPickCallback == null && onImagePickCallback == null) { - throw ArgumentError( - 'onImagePickCallback and onVideoPickCallback are both null', - ); - } - final selector = cameraPickSettingSelector ?? - (context) => showDialog( - context: context, - builder: (ctx) => AlertDialog( - contentPadding: EdgeInsets.zero, - backgroundColor: Colors.transparent, - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - if (onImagePickCallback != null) - TextButton.icon( - icon: const Icon( - Icons.camera, - color: Colors.orangeAccent, - ), - label: Text('Camera'.i18n), - onPressed: () => - Navigator.pop(ctx, MediaPickSetting.Camera), - ), - if (onVideoPickCallback != null) - TextButton.icon( - icon: const Icon( - Icons.video_call, - color: Colors.cyanAccent, - ), - label: Text('Video'.i18n), - onPressed: () => - Navigator.pop(ctx, MediaPickSetting.Video), - ) - ], - ), - ), - ); - - final source = await selector(context); - if (source == null) { - return; - } - switch (source) { - case MediaPickSetting.Camera: - await ImageVideoUtils.handleImageButtonTap( - context, - controller, - ImageSource.camera, - onImagePickCallback!, - filePickImpl: filePickImpl, - webImagePickImpl: webImagePickImpl, - ); - break; - case MediaPickSetting.Video: - await ImageVideoUtils.handleVideoButtonTap( - context, - controller, - ImageSource.camera, - onVideoPickCallback!, - filePickImpl: filePickImpl, - webVideoPickImpl: webVideoPickImpl, - ); - break; - case MediaPickSetting.Gallery: - throw ArgumentError( - 'Invalid MediaSetting for the camera button.\n' - 'gallery is not related to camera button', - ); - case MediaPickSetting.Link: - throw ArgumentError( - 'Invalid MediaSetting for the camera button.\n' - 'link is not related to camera button', - ); - } - } -} diff --git a/flutter_quill_extensions/lib/embeds/toolbar/formula_button.dart b/flutter_quill_extensions/lib/embeds/toolbar/formula_button.dart deleted file mode 100644 index 882d067c..00000000 --- a/flutter_quill_extensions/lib/embeds/toolbar/formula_button.dart +++ /dev/null @@ -1,55 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_quill/flutter_quill.dart'; - -class FormulaButton extends StatelessWidget { - const FormulaButton({ - required this.icon, - required this.controller, - this.iconSize = kDefaultIconSize, - this.fillColor, - this.iconTheme, - this.dialogTheme, - this.tooltip, - Key? key, - }) : super(key: key); - - final IconData icon; - - final double iconSize; - - final Color? fillColor; - - final QuillController controller; - - final QuillIconTheme? iconTheme; - - final QuillDialogTheme? dialogTheme; - final String? tooltip; - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - - final iconColor = iconTheme?.iconUnselectedColor ?? theme.iconTheme.color; - final iconFillColor = - iconTheme?.iconUnselectedFillColor ?? (fillColor ?? theme.canvasColor); - - return QuillToolbarIconButton( - icon: Icon(icon, size: iconSize, color: iconColor), - tooltip: tooltip, - highlightElevation: 0, - hoverElevation: 0, - size: iconSize * 1.77, - fillColor: iconFillColor, - borderRadius: iconTheme?.borderRadius ?? 2, - onPressed: () => _onPressedHandler(context), - ); - } - - Future _onPressedHandler(BuildContext context) async { - final index = controller.selection.baseOffset; - final length = controller.selection.extentOffset - index; - - controller.replaceText(index, length, BlockEmbed.formula(''), null); - } -} diff --git a/flutter_quill_extensions/lib/embeds/toolbar/image_button.dart b/flutter_quill_extensions/lib/embeds/toolbar/image_button.dart deleted file mode 100644 index af5f924f..00000000 --- a/flutter_quill_extensions/lib/embeds/toolbar/image_button.dart +++ /dev/null @@ -1,138 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_quill/flutter_quill.dart'; -import 'package:image_picker/image_picker.dart'; - -import '../embed_types.dart'; -import 'image_video_utils.dart'; - -class ImageButton extends StatelessWidget { - const ImageButton({ - required this.icon, - required this.controller, - this.iconSize = kDefaultIconSize, - this.onImagePickCallback, - this.fillColor, - this.filePickImpl, - this.webImagePickImpl, - this.mediaPickSettingSelector, - this.iconTheme, - this.dialogTheme, - this.tooltip, - this.linkRegExp, - Key? key, - }) : super(key: key); - - final IconData icon; - final double iconSize; - - final Color? fillColor; - - final QuillController controller; - - final OnImagePickCallback? onImagePickCallback; - - final WebImagePickImpl? webImagePickImpl; - - final FilePickImpl? filePickImpl; - - final MediaPickSettingSelector? mediaPickSettingSelector; - - final QuillIconTheme? iconTheme; - - final QuillDialogTheme? dialogTheme; - final String? tooltip; - final RegExp? linkRegExp; - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - - final iconColor = iconTheme?.iconUnselectedColor ?? theme.iconTheme.color; - final iconFillColor = - iconTheme?.iconUnselectedFillColor ?? (fillColor ?? theme.canvasColor); - - return QuillToolbarIconButton( - icon: Icon(icon, size: iconSize, color: iconColor), - tooltip: tooltip, - highlightElevation: 0, - hoverElevation: 0, - size: iconSize * 1.77, - fillColor: iconFillColor, - borderRadius: iconTheme?.borderRadius ?? 2, - onPressed: () => _onPressedHandler(context), - ); - } - - Future _onPressedHandler(BuildContext context) async { - final onImagePickCallbackRef = onImagePickCallback; - if (onImagePickCallbackRef == null) { - await _typeLink(context); - return; - } - final selector = - mediaPickSettingSelector ?? ImageVideoUtils.selectMediaPickSetting; - final source = await selector(context); - if (source == null) { - return; - } - switch (source) { - case MediaPickSetting.Gallery: - _pickImage(context); - break; - case MediaPickSetting.Link: - await _typeLink(context); - break; - case MediaPickSetting.Camera: - await ImageVideoUtils.handleImageButtonTap( - context, - controller, - ImageSource.camera, - onImagePickCallbackRef, - filePickImpl: filePickImpl, - webImagePickImpl: webImagePickImpl, - ); - break; - case MediaPickSetting.Video: - throw ArgumentError( - 'Sorry but this is the Image button and not the video one', - ); - } - - // This will not work for the pick image using camera (bug fix) - // if (source != null) { - // if (source == MediaPickSetting.Gallery) { - - // } else { - // _typeLink(context); - // } - } - - void _pickImage(BuildContext context) => ImageVideoUtils.handleImageButtonTap( - context, - controller, - ImageSource.gallery, - onImagePickCallback!, - filePickImpl: filePickImpl, - webImagePickImpl: webImagePickImpl, - ); - - Future _typeLink(BuildContext context) async { - final value = await showDialog( - context: context, - builder: (_) => LinkDialog( - dialogTheme: dialogTheme, - linkRegExp: linkRegExp, - ), - ); - _linkSubmitted(value); - } - - void _linkSubmitted(String? value) { - if (value != null && value.isNotEmpty) { - final index = controller.selection.baseOffset; - final length = controller.selection.extentOffset - index; - - controller.replaceText(index, length, BlockEmbed.image(value), null); - } - } -} diff --git a/flutter_quill_extensions/lib/embeds/toolbar/image_video_utils.dart b/flutter_quill_extensions/lib/embeds/toolbar/image_video_utils.dart deleted file mode 100644 index 5da99c6d..00000000 --- a/flutter_quill_extensions/lib/embeds/toolbar/image_video_utils.dart +++ /dev/null @@ -1,247 +0,0 @@ -import 'dart:io' show File; - -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/translations.dart'; -import 'package:image_picker/image_picker.dart'; - -import '../embed_types.dart'; - -class LinkDialog extends StatefulWidget { - const LinkDialog({ - this.dialogTheme, - this.link, - this.linkRegExp, - Key? key, - }) : super(key: key); - - final QuillDialogTheme? dialogTheme; - final String? link; - final RegExp? linkRegExp; - - @override - LinkDialogState createState() => LinkDialogState(); -} - -class LinkDialogState extends State { - late String _link; - late TextEditingController _controller; - late RegExp _linkRegExp; - - @override - void initState() { - super.initState(); - _link = widget.link ?? ''; - _controller = TextEditingController(text: _link); - - final defaultLinkNonSecureRegExp = RegExp( - r'https?://.*?\.(?:png|jpe?g|gif|bmp|webp|tiff?)', - caseSensitive: false, - ); // Not secure - // final defaultLinkRegExp = RegExp( - // r'https://.*?\.(?:png|jpe?g|gif|bmp|webp|tiff?)', - // caseSensitive: false, - // ); // Secure - _linkRegExp = widget.linkRegExp ?? defaultLinkNonSecureRegExp; - } - - @override - void dispose() { - _controller.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return AlertDialog( - backgroundColor: widget.dialogTheme?.dialogBackgroundColor, - content: TextField( - keyboardType: TextInputType.url, - textInputAction: TextInputAction.done, - maxLines: null, - style: widget.dialogTheme?.inputTextStyle, - decoration: InputDecoration( - labelText: 'Paste a link'.i18n, - hintText: 'Please enter a valid image url'.i18n, - labelStyle: widget.dialogTheme?.labelTextStyle, - floatingLabelStyle: widget.dialogTheme?.labelTextStyle, - ), - autofocus: true, - onChanged: _linkChanged, - controller: _controller, - onEditingComplete: () { - if (!_canPress()) { - return; - } - _applyLink(); - }, - ), - actions: [ - TextButton( - onPressed: _canPress() ? _applyLink : null, - child: Text( - 'Ok'.i18n, - style: widget.dialogTheme?.labelTextStyle, - ), - ), - ], - ); - } - - void _linkChanged(String value) { - setState(() { - _link = value; - }); - } - - void _applyLink() { - Navigator.pop(context, _link.trim()); - } - - bool _canPress() { - return _link.isNotEmpty && _linkRegExp.hasMatch(_link); - } -} - -class ImageVideoUtils { - static Future selectMediaPickSetting( - BuildContext context, - ) => - showDialog( - context: context, - builder: (ctx) => AlertDialog( - contentPadding: EdgeInsets.zero, - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - TextButton.icon( - icon: const Icon( - Icons.collections, - color: Colors.orangeAccent, - ), - label: Text('Gallery'.i18n), - onPressed: () => Navigator.pop(ctx, MediaPickSetting.Gallery), - ), - TextButton.icon( - icon: const Icon( - Icons.link, - color: Colors.cyanAccent, - ), - label: Text('Link'.i18n), - onPressed: () => Navigator.pop(ctx, MediaPickSetting.Link), - ) - ], - ), - ), - ); - - /// For image picking logic - static Future handleImageButtonTap( - BuildContext context, - QuillController controller, - ImageSource imageSource, - OnImagePickCallback onImagePickCallback, { - FilePickImpl? filePickImpl, - WebImagePickImpl? webImagePickImpl, - }) async { - final index = controller.selection.baseOffset; - final length = controller.selection.extentOffset - index; - - String? imageUrl; - if (kIsWeb) { - assert( - webImagePickImpl != null, - 'Please provide webImagePickImpl for Web ' - '(check out example directory for how to do it)'); - imageUrl = await webImagePickImpl!(onImagePickCallback); - } else if (isMobile()) { - imageUrl = await _pickImage(imageSource, onImagePickCallback); - } else { - assert(filePickImpl != null, 'Desktop must provide filePickImpl'); - imageUrl = - await _pickImageDesktop(context, filePickImpl!, onImagePickCallback); - } - - if (imageUrl != null) { - controller.replaceText(index, length, BlockEmbed.image(imageUrl), null); - } - } - - static Future _pickImage( - ImageSource source, - OnImagePickCallback onImagePickCallback, - ) async { - final pickedFile = await ImagePicker().pickImage(source: source); - if (pickedFile == null) { - return null; - } - - return onImagePickCallback(File(pickedFile.path)); - } - - static Future _pickImageDesktop( - BuildContext context, - FilePickImpl filePickImpl, - OnImagePickCallback onImagePickCallback) async { - final filePath = await filePickImpl(context); - if (filePath == null || filePath.isEmpty) return null; - - final file = File(filePath); - return onImagePickCallback(file); - } - - /// For video picking logic - static Future handleVideoButtonTap( - BuildContext context, - QuillController controller, - ImageSource videoSource, - OnVideoPickCallback onVideoPickCallback, { - FilePickImpl? filePickImpl, - WebVideoPickImpl? webVideoPickImpl, - }) async { - final index = controller.selection.baseOffset; - final length = controller.selection.extentOffset - index; - - String? videoUrl; - if (kIsWeb) { - assert( - webVideoPickImpl != null, - 'Please provide webVideoPickImpl for Web ' - '(check out example directory for how to do it)'); - videoUrl = await webVideoPickImpl!(onVideoPickCallback); - } else if (isMobile()) { - videoUrl = await _pickVideo(videoSource, onVideoPickCallback); - } else { - assert(filePickImpl != null, 'Desktop must provide filePickImpl'); - videoUrl = - await _pickVideoDesktop(context, filePickImpl!, onVideoPickCallback); - } - - if (videoUrl != null) { - controller.replaceText(index, length, BlockEmbed.video(videoUrl), null); - } - } - - static Future _pickVideo( - ImageSource source, OnVideoPickCallback onVideoPickCallback) async { - final pickedFile = await ImagePicker().pickVideo(source: source); - if (pickedFile == null) { - return null; - } - - return onVideoPickCallback(File(pickedFile.path)); - } - - static Future _pickVideoDesktop( - BuildContext context, - FilePickImpl filePickImpl, - OnVideoPickCallback onVideoPickCallback) async { - final filePath = await filePickImpl(context); - if (filePath == null || filePath.isEmpty) return null; - - final file = File(filePath); - return onVideoPickCallback(file); - } -} diff --git a/flutter_quill_extensions/lib/embeds/toolbar/media_button.dart b/flutter_quill_extensions/lib/embeds/toolbar/media_button.dart deleted file mode 100644 index a5eae3dd..00000000 --- a/flutter_quill_extensions/lib/embeds/toolbar/media_button.dart +++ /dev/null @@ -1,494 +0,0 @@ -import 'dart:math' as math; -import 'dart:ui'; - -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/translations.dart'; -import 'package:image_picker/image_picker.dart'; - -import '../embed_types.dart'; -import 'image_video_utils.dart'; - -/// Widget which combines [ImageButton] and [VideButton] widgets. This widget -/// has more customization and uses dialog similar to one which is used -/// on [http://quilljs.com]. -class MediaButton extends StatelessWidget { - const MediaButton({ - required this.controller, - required this.onImagePickCallback, - required this.onVideoPickCallback, - required this.filePickImpl, - required this.webImagePickImpl, - required this.webVideoPickImpl, - required this.icon, - this.type = QuillMediaType.image, - this.iconSize = kDefaultIconSize, - this.fillColor, - this.mediaFilePicker = _defaultMediaPicker, - this.onMediaPickedCallback, - this.iconTheme, - this.dialogTheme, - this.tooltip, - this.childrenSpacing = 16.0, - this.labelText, - this.hintText, - this.submitButtonText, - this.submitButtonSize, - this.galleryButtonText, - this.linkButtonText, - this.autovalidateMode = AutovalidateMode.disabled, - this.dialogBarrierColor = Colors.black54, - Key? key, - this.validationMessage, - }) : assert(type == QuillMediaType.image, - 'Video selection is not supported yet'), - super(key: key); - - final QuillController controller; - final IconData icon; - final double iconSize; - final Color? fillColor; - final QuillMediaType type; - final QuillIconTheme? iconTheme; - final QuillDialogTheme? dialogTheme; - final String? tooltip; - final MediaFilePicker mediaFilePicker; - final MediaPickedCallback? onMediaPickedCallback; - final Color dialogBarrierColor; - - /// The margin between child widgets in the dialog. - final double childrenSpacing; - - /// The text of label in link add mode. - final String? labelText; - - /// The hint text for link [TextField]. - final String? hintText; - - /// The text of the submit button. - final String? submitButtonText; - - /// The size of dialog buttons. - final Size? submitButtonSize; - - /// The text of the gallery button [MediaSourceSelectorDialog]. - final String? galleryButtonText; - - /// The text of the link button [MediaSourceSelectorDialog]. - final String? linkButtonText; - - final AutovalidateMode autovalidateMode; - final String? validationMessage; - final OnImagePickCallback onImagePickCallback; - final FilePickImpl? filePickImpl; - final WebImagePickImpl? webImagePickImpl; - final OnVideoPickCallback onVideoPickCallback; - final WebVideoPickImpl? webVideoPickImpl; - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - final iconColor = iconTheme?.iconUnselectedColor ?? theme.iconTheme.color; - final iconFillColor = - iconTheme?.iconUnselectedFillColor ?? fillColor ?? theme.canvasColor; - - return QuillToolbarIconButton( - icon: Icon(icon, size: iconSize, color: iconColor), - tooltip: tooltip, - highlightElevation: 0, - hoverElevation: 0, - size: iconSize * 1.77, - fillColor: iconFillColor, - borderRadius: iconTheme?.borderRadius ?? 2, - onPressed: () => _onPressedHandler(context), - ); - } - - Future _onPressedHandler(BuildContext context) async { - if (onMediaPickedCallback == null) { - _inputLink(context); - return; - } - final mediaSource = await showDialog( - context: context, - builder: (_) => MediaSourceSelectorDialog( - dialogTheme: dialogTheme, - galleryButtonText: galleryButtonText, - linkButtonText: linkButtonText, - ), - ); - if (mediaSource == null) { - return; - } - switch (mediaSource) { - case MediaPickSetting.Gallery: - await _pickImage(); - break; - case MediaPickSetting.Link: - _inputLink(context); - break; - case MediaPickSetting.Camera: - await ImageVideoUtils.handleImageButtonTap( - context, - controller, - ImageSource.camera, - onImagePickCallback, - filePickImpl: filePickImpl, - webImagePickImpl: webImagePickImpl, - ); - break; - case MediaPickSetting.Video: - await ImageVideoUtils.handleVideoButtonTap( - context, - controller, - ImageSource.camera, - onVideoPickCallback, - filePickImpl: filePickImpl, - webVideoPickImpl: webVideoPickImpl, - ); - break; - } - } - - Future _pickImage() async { - if (!(kIsWeb || isMobile() || isDesktop())) { - throw UnsupportedError( - 'Unsupported target platform: ${defaultTargetPlatform.name}', - ); - } - - final mediaFileUrl = await _pickMediaFileUrl(); - - if (mediaFileUrl != null) { - final index = controller.selection.baseOffset; - final length = controller.selection.extentOffset - index; - controller.replaceText( - index, - length, - BlockEmbed.image(mediaFileUrl), - null, - ); - } - } - - Future _pickMediaFileUrl() async { - final mediaFile = await mediaFilePicker(type); - return mediaFile != null ? onMediaPickedCallback?.call(mediaFile) : null; - } - - void _inputLink(BuildContext context) { - showDialog( - context: context, - builder: (_) => MediaLinkDialog( - dialogTheme: dialogTheme, - labelText: labelText, - hintText: hintText, - buttonText: submitButtonText, - buttonSize: submitButtonSize, - childrenSpacing: childrenSpacing, - autovalidateMode: autovalidateMode, - validationMessage: validationMessage, - ), - ).then(_linkSubmitted); - } - - void _linkSubmitted(String? value) { - if (value != null && value.isNotEmpty) { - final index = controller.selection.baseOffset; - final length = controller.selection.extentOffset - index; - final data = - type.isImage ? BlockEmbed.image(value) : BlockEmbed.video(value); - controller.replaceText(index, length, data, null); - } - } -} - -/// Provides a dialog for input link to media resource. -class MediaLinkDialog extends StatefulWidget { - const MediaLinkDialog({ - Key? key, - this.link, - this.dialogTheme, - this.childrenSpacing = 16.0, - this.labelText, - this.hintText, - this.buttonText, - this.buttonSize, - this.autovalidateMode = AutovalidateMode.disabled, - this.validationMessage, - }) : assert(childrenSpacing > 0), - super(key: key); - - final String? link; - final QuillDialogTheme? dialogTheme; - - /// The margin between child widgets in the dialog. - final double childrenSpacing; - - /// The text of label in link add mode. - final String? labelText; - - /// The hint text for link [TextField]. - final String? hintText; - - /// The text of the submit button. - final String? buttonText; - - /// The size of dialog buttons. - final Size? buttonSize; - - final AutovalidateMode autovalidateMode; - final String? validationMessage; - - @override - State createState() => _MediaLinkDialogState(); -} - -class _MediaLinkDialogState extends State { - final _linkFocus = FocusNode(); - final _linkController = TextEditingController(); - - @override - void dispose() { - _linkFocus.dispose(); - _linkController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final constraints = widget.dialogTheme?.linkDialogConstraints ?? - () { - final size = MediaQuery.sizeOf(context); - final maxWidth = kIsWeb ? size.width / 4 : size.width - 80; - return BoxConstraints(maxWidth: maxWidth, maxHeight: 80); - }(); - - final buttonStyle = widget.buttonSize != null - ? Theme.of(context) - .elevatedButtonTheme - .style - ?.copyWith(fixedSize: MaterialStatePropertyAll(widget.buttonSize)) - : widget.dialogTheme?.buttonStyle; - - final isWrappable = widget.dialogTheme?.isWrappable ?? false; - - final children = [ - Text(widget.labelText ?? 'Enter media'.i18n), - UtilityWidgets.maybeWidget( - enabled: !isWrappable, - wrapper: (child) => Expanded( - child: child, - ), - child: Padding( - padding: EdgeInsets.symmetric(horizontal: widget.childrenSpacing), - child: TextFormField( - controller: _linkController, - focusNode: _linkFocus, - style: widget.dialogTheme?.inputTextStyle, - keyboardType: TextInputType.url, - textInputAction: TextInputAction.done, - decoration: InputDecoration( - labelStyle: widget.dialogTheme?.labelTextStyle, - hintText: widget.hintText, - ), - autofocus: true, - autovalidateMode: widget.autovalidateMode, - validator: _validateLink, - onChanged: _linkChanged, - ), - ), - ), - ElevatedButton( - onPressed: _canPress() ? _submitLink : null, - style: buttonStyle, - child: Text(widget.buttonText ?? 'Ok'.i18n), - ), - ]; - - return Dialog( - backgroundColor: widget.dialogTheme?.dialogBackgroundColor, - shape: widget.dialogTheme?.shape ?? - DialogTheme.of(context).shape ?? - RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)), - child: ConstrainedBox( - constraints: constraints, - child: Padding( - padding: - widget.dialogTheme?.linkDialogPadding ?? const EdgeInsets.all(16), - child: Form( - child: isWrappable - ? Wrap( - alignment: WrapAlignment.center, - crossAxisAlignment: WrapCrossAlignment.center, - runSpacing: widget.dialogTheme?.runSpacing ?? 0.0, - children: children, - ) - : Row( - children: children, - ), - ), - ), - ), - ); - } - - bool _canPress() => _validateLink(_linkController.text) == null; - - void _linkChanged(String value) { - setState(() { - _linkController.text = value; - }); - } - - void _submitLink() => Navigator.pop(context, _linkController.text); - - String? _validateLink(String? value) { - if ((value?.isEmpty ?? false) || - !AutoFormatMultipleLinksRule.oneLineRegExp.hasMatch(value!)) { - return widget.validationMessage ?? 'That is not a valid URL'; - } - - return null; - } -} - -/// Media souce selector. -class MediaSourceSelectorDialog extends StatelessWidget { - const MediaSourceSelectorDialog({ - Key? key, - this.dialogTheme, - this.galleryButtonText, - this.linkButtonText, - }) : super(key: key); - - final QuillDialogTheme? dialogTheme; - - /// The text of the gallery button [MediaSourceSelectorDialog]. - final String? galleryButtonText; - - /// The text of the link button [MediaSourceSelectorDialog]. - final String? linkButtonText; - - @override - Widget build(BuildContext context) { - final constraints = dialogTheme?.mediaSelectorDialogConstraints ?? - () { - final size = MediaQuery.sizeOf(context); - double maxWidth, maxHeight; - if (kIsWeb) { - maxWidth = size.width / 7; - maxHeight = size.height / 7; - } else { - maxWidth = size.width - 80; - maxHeight = maxWidth / 2; - } - return BoxConstraints(maxWidth: maxWidth, maxHeight: maxHeight); - }(); - - final shape = dialogTheme?.shape ?? - DialogTheme.of(context).shape ?? - RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)); - - return Dialog( - backgroundColor: dialogTheme?.dialogBackgroundColor, - shape: shape, - child: ConstrainedBox( - constraints: constraints, - child: Padding( - padding: dialogTheme?.mediaSelectorDialogPadding ?? - const EdgeInsets.all(16), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: TextButtonWithIcon( - icon: Icons.collections, - label: galleryButtonText ?? 'Gallery'.i18n, - onPressed: () => - Navigator.pop(context, MediaPickSetting.Gallery), - ), - ), - const SizedBox(width: 10), - Expanded( - child: TextButtonWithIcon( - icon: Icons.link, - label: linkButtonText ?? 'Link'.i18n, - onPressed: () => - Navigator.pop(context, MediaPickSetting.Link), - ), - ) - ], - ), - ), - ), - ); - } -} - -class TextButtonWithIcon extends StatelessWidget { - const TextButtonWithIcon({ - required this.label, - required this.icon, - required this.onPressed, - this.textStyle, - Key? key, - }) : super(key: key); - - final String label; - final IconData icon; - final VoidCallback onPressed; - final TextStyle? textStyle; - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - final scale = MediaQuery.maybeOf(context)?.textScaleFactor ?? 1; - final gap = scale <= 1 ? 8.0 : lerpDouble(8, 4, math.min(scale - 1, 1))!; - final buttonStyle = TextButtonTheme.of(context).style; - final shape = buttonStyle?.shape?.resolve({}) ?? - const RoundedRectangleBorder( - borderRadius: BorderRadius.all(Radius.circular(4))); - return Material( - shape: shape, - textStyle: textStyle ?? - theme.textButtonTheme.style?.textStyle?.resolve({}) ?? - theme.textTheme.labelLarge, - elevation: buttonStyle?.elevation?.resolve({}) ?? 0, - child: InkWell( - customBorder: shape, - onTap: onPressed, - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Icon(icon), - SizedBox(height: gap), - Flexible(child: Text(label)), - ], - ), - ), - ), - ); - } -} - -/// Default file picker. -Future _defaultMediaPicker(QuillMediaType mediaType) async { - final pickedFile = mediaType.isImage - ? await ImagePicker().pickImage(source: ImageSource.gallery) - : await ImagePicker().pickVideo(source: ImageSource.gallery); - - if (pickedFile != null) { - return QuillFile( - name: pickedFile.name, - path: pickedFile.path, - bytes: await pickedFile.readAsBytes(), - ); - } - - return null; -} diff --git a/flutter_quill_extensions/lib/embeds/toolbar/video_button.dart b/flutter_quill_extensions/lib/embeds/toolbar/video_button.dart deleted file mode 100644 index f09d193b..00000000 --- a/flutter_quill_extensions/lib/embeds/toolbar/video_button.dart +++ /dev/null @@ -1,110 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_quill/flutter_quill.dart'; -import 'package:image_picker/image_picker.dart'; - -import '../embed_types.dart'; -import 'image_video_utils.dart'; - -class VideoButton extends StatelessWidget { - const VideoButton({ - required this.icon, - required this.controller, - this.iconSize = kDefaultIconSize, - this.onVideoPickCallback, - this.fillColor, - this.filePickImpl, - this.webVideoPickImpl, - this.mediaPickSettingSelector, - this.iconTheme, - this.dialogTheme, - this.tooltip, - this.linkRegExp, - Key? key, - }) : super(key: key); - - final IconData icon; - final double iconSize; - - final Color? fillColor; - - final QuillController controller; - - final OnVideoPickCallback? onVideoPickCallback; - - final WebVideoPickImpl? webVideoPickImpl; - - final FilePickImpl? filePickImpl; - - final MediaPickSettingSelector? mediaPickSettingSelector; - - final QuillIconTheme? iconTheme; - - final QuillDialogTheme? dialogTheme; - - final String? tooltip; - - final RegExp? linkRegExp; - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - - final iconColor = iconTheme?.iconUnselectedColor ?? theme.iconTheme.color; - final iconFillColor = - iconTheme?.iconUnselectedFillColor ?? (fillColor ?? theme.canvasColor); - - return QuillToolbarIconButton( - icon: Icon(icon, size: iconSize, color: iconColor), - tooltip: tooltip, - highlightElevation: 0, - hoverElevation: 0, - size: iconSize * 1.77, - fillColor: iconFillColor, - borderRadius: iconTheme?.borderRadius ?? 2, - onPressed: () => _onPressedHandler(context), - ); - } - - Future _onPressedHandler(BuildContext context) async { - if (onVideoPickCallback != null) { - final selector = - mediaPickSettingSelector ?? ImageVideoUtils.selectMediaPickSetting; - final source = await selector(context); - if (source != null) { - if (source == MediaPickSetting.Gallery) { - _pickVideo(context); - } else { - await _typeLink(context); - } - } - } else { - await _typeLink(context); - } - } - - void _pickVideo(BuildContext context) => ImageVideoUtils.handleVideoButtonTap( - context, - controller, - ImageSource.gallery, - onVideoPickCallback!, - filePickImpl: filePickImpl, - webVideoPickImpl: webVideoPickImpl, - ); - - Future _typeLink(BuildContext context) async { - final value = await showDialog( - context: context, - builder: (_) => LinkDialog(dialogTheme: dialogTheme), - ); - _linkSubmitted(value); - } - - void _linkSubmitted(String? value) { - if (value != null && value.isNotEmpty) { - final index = controller.selection.baseOffset; - final length = controller.selection.extentOffset - index; - - controller.replaceText(index, length, BlockEmbed.video(value), null); - } - } -} diff --git a/flutter_quill_extensions/lib/embeds/utils.dart b/flutter_quill_extensions/lib/embeds/utils.dart deleted file mode 100644 index 9d92f149..00000000 --- a/flutter_quill_extensions/lib/embeds/utils.dart +++ /dev/null @@ -1,106 +0,0 @@ -import 'dart:io' show File; - -import 'package:flutter/foundation.dart' show Uint8List, immutable; -import 'package:gal/gal.dart'; -import 'package:http/http.dart' as http; - -// I would like to orgnize the project structure and the code more -// but here I don't want to change too much since that is a community project - -RegExp _base64 = RegExp( - r'^(?:[A-Za-z0-9+\/]{4})*(?:[A-Za-z0-9+\/]{2}==|[A-Za-z0-9+\/]{3}=|[A-Za-z0-9+\/]{4})$', -); - -bool isBase64(String str) { - return _base64.hasMatch(str); -} - -bool isHttpBasedUrl(String url) { - try { - final uri = Uri.parse(url.trim()); - return uri.isScheme('HTTP') || uri.isScheme('HTTPS'); - } catch (_) { - return false; - } -} - -bool isYouTubeUrl(String videoUrl) { - try { - final uri = Uri.parse(videoUrl); - return uri.host == 'www.youtube.com' || - uri.host == 'youtube.com' || - uri.host == 'youtu.be'; - } catch (_) { - return false; - } -} - -bool isImageBase64(String imageUrl) { - return !isHttpBasedUrl(imageUrl) && isBase64(imageUrl); -} - -enum SaveImageResultMethod { network, localStorage } - -@immutable -class _SaveImageResult { - const _SaveImageResult({required this.isSuccess, required this.method}); - - final bool isSuccess; - final SaveImageResultMethod method; -} - -Future<_SaveImageResult> saveImage(String imageUrl) async { - final imageFile = File(imageUrl); - final hasPermission = await Gal.hasAccess(); - final imageExistsLocally = await imageFile.exists(); - if (!hasPermission) { - await Gal.requestAccess(); - } - if (!imageExistsLocally) { - final success = await _saveNetworkImageToLocal(imageUrl); - return _SaveImageResult( - isSuccess: success, - method: SaveImageResultMethod.network, - ); - } - final success = await _saveImageLocally(imageFile); - return _SaveImageResult( - isSuccess: success, - method: SaveImageResultMethod.localStorage, - ); -} - -Future _saveNetworkImageToLocal(String imageUrl) async { - try { - final response = await http.get( - Uri.parse(imageUrl), - ); - if (response.statusCode != 200) { - return false; - } - final imageBytes = response.bodyBytes; - await Gal.putImageBytes(imageBytes); - return true; - } catch (e) { - return false; - } -} - -Future _convertFileToUint8List(File file) async { - try { - final uint8list = await file.readAsBytes(); - return uint8list; - } catch (e) { - return Uint8List(0); - } -} - -Future _saveImageLocally(File imageFile) async { - try { - final imageBytes = await _convertFileToUint8List(imageFile); - await Gal.putImageBytes(imageBytes); - return true; - } catch (e) { - return false; - } -} diff --git a/flutter_quill_extensions/lib/embeds/widgets/image_resizer.dart b/flutter_quill_extensions/lib/embeds/widgets/image_resizer.dart deleted file mode 100644 index 9f84ad17..00000000 --- a/flutter_quill_extensions/lib/embeds/widgets/image_resizer.dart +++ /dev/null @@ -1,124 +0,0 @@ -import 'package:flutter/cupertino.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/scheduler.dart'; -import 'package:flutter_quill/translations.dart'; - -class ImageResizer extends StatefulWidget { - const ImageResizer( - {required this.imageWidth, - required this.imageHeight, - required this.maxWidth, - required this.maxHeight, - required this.onImageResize, - Key? key}) - : super(key: key); - - final double? imageWidth; - final double? imageHeight; - final double maxWidth; - final double maxHeight; - final Function(double, double) onImageResize; - - @override - _ImageResizerState createState() => _ImageResizerState(); -} - -class _ImageResizerState extends State { - late double _width; - late double _height; - - @override - void initState() { - super.initState(); - _width = widget.imageWidth ?? widget.maxWidth; - _height = widget.imageHeight ?? widget.maxHeight; - } - - @override - Widget build(BuildContext context) { - switch (defaultTargetPlatform) { - case TargetPlatform.iOS: - return _showCupertinoMenu(); - case TargetPlatform.android: - return _showMaterialMenu(); - case TargetPlatform.macOS: - case TargetPlatform.windows: - case TargetPlatform.linux: - case TargetPlatform.fuchsia: - return _showMaterialMenu(); - default: - throw 'Not supposed to be invoked for $defaultTargetPlatform'; - } - } - - Widget _showMaterialMenu() { - return Column( - mainAxisSize: MainAxisSize.min, - children: [_widthSlider(), _heightSlider()], - ); - } - - Widget _showCupertinoMenu() { - return CupertinoActionSheet(actions: [ - CupertinoActionSheetAction( - onPressed: () {}, - child: _widthSlider(), - ), - CupertinoActionSheetAction( - onPressed: () {}, - child: _heightSlider(), - ) - ]); - } - - Widget _slider( - double value, - double max, - String label, - ValueChanged onChanged, - ) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 8), - child: Card( - child: Slider( - value: value, - max: max, - divisions: 1000, - label: label.i18n, - onChanged: (val) { - setState(() { - onChanged(val); - _resizeImage(); - }); - }, - ), - )); - } - - Widget _heightSlider() { - return _slider(_height, widget.maxHeight, 'Height', (value) { - _height = value; - }); - } - - Widget _widthSlider() { - return _slider(_width, widget.maxWidth, 'Width', (value) { - _width = value; - }); - } - - bool _scheduled = false; - - void _resizeImage() { - if (_scheduled) { - return; - } - - _scheduled = true; - SchedulerBinding.instance.addPostFrameCallback((_) { - widget.onImageResize(_width, _height); - _scheduled = false; - }); - } -} diff --git a/flutter_quill_extensions/lib/flutter_quill_extensions.dart b/flutter_quill_extensions/lib/flutter_quill_extensions.dart index 0fc6a54e..99e598de 100644 --- a/flutter_quill_extensions/lib/flutter_quill_extensions.dart +++ b/flutter_quill_extensions/lib/flutter_quill_extensions.dart @@ -1,27 +1,60 @@ library flutter_quill_extensions; -import 'package:flutter/material.dart'; -import 'package:flutter_quill/extensions.dart'; +import 'package:flutter/foundation.dart' show kIsWeb; import 'package:flutter_quill/flutter_quill.dart'; +import 'package:meta/meta.dart' show immutable; -import 'embeds/builders.dart'; -import 'embeds/embed_types.dart'; -import 'embeds/toolbar/camera_button.dart'; -import 'embeds/toolbar/formula_button.dart'; -import 'embeds/toolbar/image_button.dart'; -import 'embeds/toolbar/media_button.dart'; -import 'embeds/toolbar/video_button.dart'; +import 'presentation/embeds/editor/image/image.dart'; +import 'presentation/embeds/editor/image/image_web.dart'; +import 'presentation/embeds/editor/video/video.dart'; +import 'presentation/embeds/editor/video/video_web.dart'; +import 'presentation/embeds/editor/webview.dart'; +import 'presentation/embeds/toolbar/camera_button/camera_button.dart'; +import 'presentation/embeds/toolbar/image_button/image_button.dart'; +import 'presentation/embeds/toolbar/video_button/video_button.dart'; +import 'presentation/models/config/editor/image/image.dart'; +import 'presentation/models/config/editor/image/image_web.dart'; +import 'presentation/models/config/editor/video/video.dart'; +import 'presentation/models/config/editor/video/video_web.dart'; +import 'presentation/models/config/editor/webview.dart'; +import 'presentation/models/config/toolbar/buttons/camera.dart'; +import 'presentation/models/config/toolbar/buttons/image.dart'; +import 'presentation/models/config/toolbar/buttons/media_button.dart'; +import 'presentation/models/config/toolbar/buttons/video.dart'; -export 'embeds/embed_types.dart'; -export 'embeds/toolbar/camera_button.dart'; -export 'embeds/toolbar/formula_button.dart'; -export 'embeds/toolbar/image_button.dart'; -export 'embeds/toolbar/image_video_utils.dart'; -export 'embeds/toolbar/media_button.dart'; -export 'embeds/toolbar/video_button.dart'; -export 'embeds/utils.dart'; +export '/logic/extensions/controller.dart'; +export '/presentation/models/config/editor/webview.dart'; +export 'logic/models/config/shared_configurations.dart'; +export 'presentation/embeds/editor/image/image.dart'; +export 'presentation/embeds/editor/image/image_web.dart'; +export 'presentation/embeds/editor/unknown.dart'; +export 'presentation/embeds/editor/video/video.dart'; +export 'presentation/embeds/editor/video/video_web.dart'; +export 'presentation/embeds/editor/webview.dart'; +export 'presentation/embeds/embed_types.dart'; +export 'presentation/embeds/embed_types/image.dart'; +export 'presentation/embeds/embed_types/video.dart'; +export 'presentation/embeds/toolbar/camera_button/camera_button.dart'; +export 'presentation/embeds/toolbar/formula_button.dart'; +export 'presentation/embeds/toolbar/image_button/image_button.dart'; +export 'presentation/embeds/toolbar/media_button/media_button.dart'; +export 'presentation/embeds/toolbar/utils/image_video_utils.dart'; +export 'presentation/embeds/toolbar/video_button/video_button.dart'; +export 'presentation/models/config/editor/image/image.dart'; +export 'presentation/models/config/editor/image/image_web.dart'; +export 'presentation/models/config/editor/video/video.dart'; +export 'presentation/models/config/editor/video/video_web.dart'; +export 'presentation/models/config/toolbar/buttons/camera.dart'; +export 'presentation/models/config/toolbar/buttons/formula.dart'; +export 'presentation/models/config/toolbar/buttons/image.dart'; +export 'presentation/models/config/toolbar/buttons/media_button.dart'; +export 'presentation/models/config/toolbar/buttons/video.dart'; +export 'presentation/utils/utils.dart'; +@immutable class FlutterQuillEmbeds { + const FlutterQuillEmbeds._(); + /// Returns a list of embed builders for QuillEditor. /// /// This method provides a collection of embed builders to enhance the @@ -32,179 +65,96 @@ class FlutterQuillEmbeds { /// /// **Note:** This method is not intended for web usage. /// For web-specific embeds, - /// use [webBuilders]. - /// - /// [onVideoInit] is a callback function that gets triggered when - /// a video is initialized. - /// You can use this to perform actions or setup configurations related - /// to video embedding. - /// - /// [onImageRemovedCallback] is called when an image is - /// removed from the editor. - /// By default, [onImageRemovedCallback] deletes the - /// temporary image file if - /// the platform is mobile and if it still exists. You - /// can customize this behavior - /// by passing your own function that handles the removal process. - /// - /// Example of [onImageRemovedCallback] customization: - /// ```dart - /// afterRemoveImageFromEditor: (imageFile) async { - /// // Your custom logic here - /// // or leave it empty to do nothing - /// } - /// ``` - /// - /// [shouldRemoveImageCallback] is a callback - /// function that is invoked when the - /// user attempts to remove an image from the editor. It allows you to control - /// whether the image should be removed based on your custom logic. - /// - /// Example of [shouldRemoveImageCallback] customization: - /// ```dart - /// shouldRemoveImageFromEditor: (imageFile) async { - /// // Show a confirmation dialog before removing the image - /// final isShouldRemove = await showYesCancelDialog( - /// context: context, - /// options: const YesOrCancelDialogOptions( - /// title: 'Deleting an image', - /// message: 'Are you sure you want' ' to delete this - /// image from the editor?', - /// ), - /// ); - /// - /// // Return `true` to allow image removal if the user confirms, otherwise - /// `false` - /// return isShouldRemove; - /// } - /// ``` + /// use [editorWebBuilders]. /// - /// [imageProviderBuilder] if you want to use custom image provider, please - /// pass a value to this property - /// By default we will use [NetworkImage] provider if the image url/path - /// is using http/https, if not then we will use [FileImage] provider - /// If you ovveride this make sure to handle the case where if the [imageUrl] - /// is in the local storage or it does exists in the system file - /// or use the same way we did it - /// - /// Example of [imageProviderBuilder] customization: - /// ```dart - /// imageProviderBuilder: (imageUrl) async { - /// // Example of using cached_network_image package - /// // Don't forgot to check if that image is local or network one - /// return CachedNetworkImageProvider(imageUrl); - /// } - /// ``` - /// - /// [imageErrorWidgetBuilder] if you want to show a custom widget based on the - /// exception that happen while loading the image, if it network image or - /// local one, and it will get called on all the images even in the photo - /// preview widget and not just in the quill editor - /// by default the default error from flutter framework will thrown - /// - /// [forceUseMobileOptionMenuForImageClick] is a boolean - /// flag that, when set to `true`, - /// enforces the use of the mobile-specific option menu for image clicks in - /// other platforms like desktop, this option doesn't affect mobile. it will - /// not affect web - /// This option - /// can be used to override the default behavior based on the platform. /// /// The method returns a list of [EmbedBuilder] objects that can be used with /// QuillEditor /// to enable embedded content features like images, videos, and formulas. /// - /// Example usage: - /// ```dart - /// final embedBuilders = QuillEmbedBuilders.builders( - /// onVideoInit: (videoContainerKey) { - /// // Custom video initialization logic - /// }, - /// // Customize other callback functions as needed - /// ); /// /// final quillEditor = QuillEditor( /// // Other editor configurations /// embedBuilders: embedBuilders, /// ); /// ``` - static List builders({ - void Function(GlobalKey videoContainerKey)? onVideoInit, - ImageEmbedBuilderOnRemovedCallback? onImageRemovedCallback, - ImageEmbedBuilderWillRemoveCallback? shouldRemoveImageCallback, - ImageEmbedBuilderProviderBuilder? imageProviderBuilder, - ImageEmbedBuilderErrorWidgetBuilder? imageErrorWidgetBuilder, - bool forceUseMobileOptionMenuForImageClick = false, - }) => - [ - ImageEmbedBuilder( - imageErrorWidgetBuilder: imageErrorWidgetBuilder, - imageProviderBuilder: imageProviderBuilder, - forceUseMobileOptionMenu: forceUseMobileOptionMenuForImageClick, - onImageRemovedCallback: onImageRemovedCallback ?? - (imageFile) async { - final mobile = isMobile(); - // If the platform is not mobile, return void; - // Since the mobile OS gives us a copy of the image - - // Note: We should remove the image on Flutter web - // since the behavior is similar to how it is on mobile, - // but since this builder is not for web, we will ignore it - if (!mobile) { - return; - } - - // On mobile OS (Android, iOS), the system will not give us - // direct access to the image; instead, - // it will give us the image - // in the temp directory of the application. So, we want to - // remove it when we no longer need it. - - // but on desktop we don't want to touch user files - // especially on macOS, where we can't even delete it without - // permission - - final isFileExists = await imageFile.exists(); - if (isFileExists) { - await imageFile.delete(); - } - }, - shouldRemoveImageCallback: shouldRemoveImageCallback, + /// + /// if you don't want image embed in your quill editor then please pass null + /// to [imageEmbedConfigurations]. same apply to [videoEmbedConfigurations] + static List editorBuilders({ + QuillEditorImageEmbedConfigurations? imageEmbedConfigurations = + const QuillEditorImageEmbedConfigurations(), + QuillEditorVideoEmbedConfigurations? videoEmbedConfigurations = + const QuillEditorVideoEmbedConfigurations(), + QuillEditorWebViewEmbedConfigurations? webViewEmbedConfigurations = + const QuillEditorWebViewEmbedConfigurations(), + }) { + if (kIsWeb) { + throw UnsupportedError( + 'The editorBuilders() is not for web, please use editorBuilders() ' + 'instead', + ); + } + return [ + if (imageEmbedConfigurations != null) + QuillEditorImageEmbedBuilder( + configurations: imageEmbedConfigurations, ), - VideoEmbedBuilder(onVideoInit: onVideoInit), - FormulaEmbedBuilder(), - ]; + if (videoEmbedConfigurations != null) + QuillEditorVideoEmbedBuilder( + configurations: videoEmbedConfigurations, + ), + if (webViewEmbedConfigurations != null) + QuillEditorWebViewEmbedBuilder( + configurations: webViewEmbedConfigurations, + ) + ]; + } /// Returns a list of embed builders specifically designed for web support. /// - /// [ImageEmbedBuilderWeb] is the embed builder for handling + /// [QuillEditorWebImageEmbedBuilder] is the embed builder for handling /// images on the web. /// - static List webBuilders() => [ - ImageEmbedBuilderWeb(), - ]; + /// [QuillEditorWebVideoEmbedBuilder] is the embed builder for handling + /// videos iframe on the web. + /// + static List editorWebBuilders( + {QuillEditorWebImageEmbedConfigurations? imageEmbedConfigurations = + const QuillEditorWebImageEmbedConfigurations(), + QuillEditorWebVideoEmbedConfigurations? videoEmbedConfigurations = + const QuillEditorWebVideoEmbedConfigurations()}) { + if (!kIsWeb) { + throw UnsupportedError( + 'The editorsWebBuilders() is only for web, please use editorBuilders() ' + 'instead for other platforms', + ); + } + return [ + if (imageEmbedConfigurations != null) + QuillEditorWebImageEmbedBuilder( + configurations: imageEmbedConfigurations, + ), + if (videoEmbedConfigurations != null) + QuillEditorWebVideoEmbedBuilder( + configurations: videoEmbedConfigurations, + ), + ]; + } - /// Returns a list of embed button builders to customize the toolbar buttons. + /// Returns a list of default embed builders for QuillEditor. /// - /// [showImageButton] determines whether the image button should be displayed. - /// [showVideoButton] determines whether the video button should be displayed. - /// [showCameraButton] determines whether the camera button should - /// be displayed. - /// [showFormulaButton] determines whether the formula button - /// should be displayed. + /// It will use [editorWebBuilders] for web and [editorBuilders] for others /// - /// [imageButtonTooltip] specifies the tooltip text for the image button. - /// [videoButtonTooltip] specifies the tooltip text for the video button. - /// [cameraButtonTooltip] specifies the tooltip text for the camera button. - /// [formulaButtonTooltip] specifies the tooltip text for the formula button. - /// - /// [onImagePickCallback] is a callback function called when an - /// image is picked. - /// [onVideoPickCallback] is a callback function called when a - /// video is picked. + /// It's not customizable with minimal configurations + static List defaultEditorBuilders() { + return kIsWeb ? editorWebBuilders() : editorBuilders(); + } + + /// Returns a list of embed button builders to customize the toolbar buttons. /// - /// [mediaPickSettingSelector] allows customizing media pick settings. - /// [cameraPickSettingSelector] allows customizing camera pick settings. + /// If you don't want to show one of the buttons for soem reason, + /// pass null to the options of it /// /// Example of customizing media pick settings for the image button: /// ```dart @@ -222,110 +172,49 @@ class FlutterQuillEmbeds { /// } /// ``` /// - /// [filePickImpl] is an implementation for picking files. - /// [webImagePickImpl] is an implementation for picking web images. - /// [webVideoPickImpl] is an implementation for picking web videos. - /// - /// [imageLinkRegExp] is a regular expression to identify image links. - /// [videoLinkRegExp] is a regular expression to identify video links. /// /// The returned list contains embed button builders for the Quill toolbar. - static List buttons({ - bool showImageButton = true, - bool showVideoButton = true, - bool showCameraButton = true, - bool showImageMediaButton = false, - bool showFormulaButton = false, - String? imageButtonTooltip, - String? videoButtonTooltip, - String? cameraButtonTooltip, - String? formulaButtonTooltip, - OnImagePickCallback? onImagePickCallback, - OnVideoPickCallback? onVideoPickCallback, - MediaPickSettingSelector? mediaPickSettingSelector, - MediaPickSettingSelector? cameraPickSettingSelector, - MediaPickedCallback? onImageMediaPickedCallback, - FilePickImpl? filePickImpl, - WebImagePickImpl? webImagePickImpl, - WebVideoPickImpl? webVideoPickImpl, - RegExp? imageLinkRegExp, - RegExp? videoLinkRegExp, + /// the [formulaButtonOptions] will be disabled by default on web + static List toolbarButtons({ + QuillToolbarImageButtonOptions? imageButtonOptions = + const QuillToolbarImageButtonOptions(), + QuillToolbarVideoButtonOptions? videoButtonOptions = + const QuillToolbarVideoButtonOptions(), + QuillToolbarCameraButtonOptions? cameraButtonOptions, + QuillToolbarMediaButtonOptions? mediaButtonOptions, }) => [ - if (showImageButton) - (controller, toolbarIconSize, iconTheme, dialogTheme) => ImageButton( - icon: Icons.image, - iconSize: toolbarIconSize, - tooltip: imageButtonTooltip, - controller: controller, - onImagePickCallback: onImagePickCallback, - filePickImpl: filePickImpl, - webImagePickImpl: webImagePickImpl, - mediaPickSettingSelector: mediaPickSettingSelector, - iconTheme: iconTheme, - dialogTheme: dialogTheme, - linkRegExp: imageLinkRegExp, - ), - if (showVideoButton) - (controller, toolbarIconSize, iconTheme, dialogTheme) => VideoButton( - icon: Icons.movie_creation, - iconSize: toolbarIconSize, - tooltip: videoButtonTooltip, - controller: controller, - onVideoPickCallback: onVideoPickCallback, - filePickImpl: filePickImpl, - webVideoPickImpl: webImagePickImpl, - mediaPickSettingSelector: mediaPickSettingSelector, - iconTheme: iconTheme, - dialogTheme: dialogTheme, - linkRegExp: videoLinkRegExp, - ), - if ((onImagePickCallback != null || onVideoPickCallback != null) && - showCameraButton) - (controller, toolbarIconSize, iconTheme, dialogTheme) => CameraButton( - icon: Icons.photo_camera, - iconSize: toolbarIconSize, - tooltip: cameraButtonTooltip, - controller: controller, - onImagePickCallback: onImagePickCallback, - onVideoPickCallback: onVideoPickCallback, - filePickImpl: filePickImpl, - webImagePickImpl: webImagePickImpl, - webVideoPickImpl: webVideoPickImpl, - cameraPickSettingSelector: cameraPickSettingSelector, - iconTheme: iconTheme, + if (imageButtonOptions != null) + (controller, toolbarIconSize, iconTheme, dialogTheme) => + QuillToolbarImageButton( + controller: imageButtonOptions.controller ?? controller, + options: imageButtonOptions, ), - if (showImageMediaButton) - (controller, toolbarIconSize, iconTheme, dialogTheme) => MediaButton( - controller: controller, - dialogTheme: dialogTheme, - iconTheme: iconTheme, - iconSize: toolbarIconSize, - onMediaPickedCallback: onImageMediaPickedCallback, - onImagePickCallback: onImagePickCallback ?? - (throw ArgumentError.notNull( - 'onImagePickCallback is required when showCameraButton is' - ' true', - )), - onVideoPickCallback: onVideoPickCallback ?? - (throw ArgumentError.notNull( - 'onVideoPickCallback is required when showCameraButton is' - ' true', - )), - filePickImpl: filePickImpl, - webImagePickImpl: webImagePickImpl, - webVideoPickImpl: webVideoPickImpl, - icon: Icons.perm_media, + if (videoButtonOptions != null) + (controller, toolbarIconSize, iconTheme, dialogTheme) => + QuillToolbarVideoButton( + controller: videoButtonOptions.controller ?? controller, + options: videoButtonOptions, ), - if (showFormulaButton) + if (cameraButtonOptions != null) (controller, toolbarIconSize, iconTheme, dialogTheme) => - FormulaButton( - icon: Icons.functions, - iconSize: toolbarIconSize, - tooltip: formulaButtonTooltip, - controller: controller, - iconTheme: iconTheme, - dialogTheme: dialogTheme, + QuillToolbarCameraButton( + controller: cameraButtonOptions.controller ?? controller, + options: cameraButtonOptions, ), + // TODO: We will return the support for this later + // if (mediaButtonOptions != null) + // (controller, toolbarIconSize, iconTheme, dialogTheme) => + // QuillToolbarMediaButton( + // controller: mediaButtonOptions.controller ?? controller, + // options: mediaButtonOptions, + // ), + // Drop the support for formula button for now + // if (formulaButtonOptions != null) + // (controller, toolbarIconSize, iconTheme, dialogTheme) => + // QuillToolbarFormulaButton( + // controller: formulaButtonOptions.controller ?? controller, + // options: formulaButtonOptions, + // ), ]; } diff --git a/flutter_quill_extensions/lib/logic/extensions/attribute.dart b/flutter_quill_extensions/lib/logic/extensions/attribute.dart new file mode 100644 index 00000000..fd939e77 --- /dev/null +++ b/flutter_quill_extensions/lib/logic/extensions/attribute.dart @@ -0,0 +1,30 @@ +import 'package:flutter_quill/flutter_quill.dart' + show Attribute, AttributeScope; + +class MobileWidthAttribute extends Attribute { + const MobileWidthAttribute(String? val) + : super('mobileWidth', AttributeScope.ignore, val); +} + +class MobileHeightAttribute extends Attribute { + const MobileHeightAttribute(String? val) + : super('mobileHeight', AttributeScope.ignore, val); +} + +class MobileMarginAttribute extends Attribute { + const MobileMarginAttribute(String? val) + : super('mobileMargin', AttributeScope.ignore, val); +} + +class MobileAlignmentAttribute extends Attribute { + const MobileAlignmentAttribute(String? val) + : super('mobileAlignment', AttributeScope.ignore, val); +} + +extension AttributeExt on Attribute { + static const MobileWidthAttribute mobileWidth = MobileWidthAttribute(null); + static const MobileHeightAttribute mobileHeight = MobileHeightAttribute(null); + static const MobileMarginAttribute mobileMargin = MobileMarginAttribute(null); + static const MobileAlignmentAttribute mobileAlignment = + MobileAlignmentAttribute(null); +} diff --git a/flutter_quill_extensions/lib/logic/extensions/controller.dart b/flutter_quill_extensions/lib/logic/extensions/controller.dart new file mode 100644 index 00000000..16625d12 --- /dev/null +++ b/flutter_quill_extensions/lib/logic/extensions/controller.dart @@ -0,0 +1,74 @@ +import 'package:flutter_quill/flutter_quill.dart'; + +import '../../presentation/embeds/editor/webview.dart'; +import '../utils/quill_image_utils.dart'; + +/// Extension functions on [QuillController] +/// that make it easier to insert the embed blocks +/// +/// and provide some other extra utilities +extension QuillControllerExt on QuillController { + int get index => selection.baseOffset; + int get length => selection.extentOffset - index; + + /// Insert webview embed block, it requires [initialUrl] to load + /// the initial page + void insertWebViewBlock({ + required String initialUrl, + }) { + final block = BlockEmbed.custom( + QuillEditorWebViewBlockEmbed( + initialUrl, + ), + ); + + this + ..skipRequestKeyboard = true + ..replaceText( + index, + length, + block, + null, + ); + } + + /// Insert image embed block, it requires the [imageSource] + /// + /// it could be local image on the system file + /// http image on the network + /// + /// image base 64 + void insertImageBlock({ + required String imageSource, + }) { + this + ..skipRequestKeyboard = true + ..replaceText( + index, + length, + BlockEmbed.image(imageSource), + null, + ); + } + + /// Insert video embed block, it requires the [videoUrl] + /// + /// it could be the video url directly (.mp4) + /// either locally on file system + /// or http video on the network + /// + /// it also supports youtube video url + void insertVideoBlock({ + required String videoUrl, + }) { + this + ..skipRequestKeyboard = true + ..replaceText(index, length, BlockEmbed.video(videoUrl), null); + } + + QuillImageUtilities get imageUtilities { + return QuillImageUtilities( + controller: this, + ); + } +} diff --git a/flutter_quill_extensions/lib/logic/models/config/shared_configurations.dart b/flutter_quill_extensions/lib/logic/models/config/shared_configurations.dart new file mode 100644 index 00000000..1badf553 --- /dev/null +++ b/flutter_quill_extensions/lib/logic/models/config/shared_configurations.dart @@ -0,0 +1,107 @@ +import 'package:flutter/widgets.dart' show BuildContext; +import 'package:flutter_quill/flutter_quill.dart'; +import 'package:meta/meta.dart' show immutable; + +import '../../services/image_picker/s_image_picker.dart'; +import '../../services/image_saver/s_image_saver.dart'; + +/// Configurations for Flutter Quill Extensions +/// that is shared between the toolbar and editor for the extensions package +/// +/// Example on how to setup it: +/// +/// ```dart +/// QuillProvider( +/// configurations: QuillConfigurations( +/// sharedConfigurations: const QuillSharedConfigurations( +/// extraConfigurations: { +/// QuillSharedExtensionsConfigurations.key: +/// QuillSharedExtensionsConfigurations( +/// // Feel free to explore it +/// ), +/// }, +/// ), +/// controller: _controller, +/// ), +/// child: const Column( +/// children: [ +/// // QuillToolbar +/// // QuillEditor +/// // ... +/// ], +// ), +/// ) +/// ``` +/// +@immutable +class QuillSharedExtensionsConfigurations { + const QuillSharedExtensionsConfigurations({ + ImagePickerService? imagePickerService, + ImageSaverService? imageSaverService, + this.assetsPrefix = 'assets', + }) : _imagePickerService = imagePickerService, + _imageSaverService = imageSaverService; + + /// Get the instance from the widget tree in [QuillSharedConfigurations] + /// if it doesn't exists, we will create new one with default options + factory QuillSharedExtensionsConfigurations.get({ + required BuildContext context, + }) { + final quillSharedExtensionsConfigurations = + context.requireQuillSharedConfigurations.extraConfigurations[key]; + if (quillSharedExtensionsConfigurations != null) { + if (quillSharedExtensionsConfigurations + is! QuillSharedExtensionsConfigurations) { + throw ArgumentError( + 'The value of key `$key` should be of type ' + 'QuillSharedExtensionsConfigurations', + ); + } + return quillSharedExtensionsConfigurations; + } + return const QuillSharedExtensionsConfigurations(); + } + + /// The key to be used in the `extraConfigurations` property + /// which can be found in the [QuillSharedConfigurations] + /// which lives in the [QuillConfigurations] + /// + /// which exists in the [QuillProvider] + static const String key = 'quillSharedExtensionsConfigurations'; + + /// Defaults to [ImagePickerService.defaultImpl] + final ImagePickerService? _imagePickerService; + + /// A getter method which returns the [ImagePickerService] that is provided + /// by the developer, if it can't be found then we will use default impl + ImagePickerService get imagePickerService { + return _imagePickerService ?? ImagePickerService.defaultImpl(); + } + + /// Default to [ImageSaverService.defaultImpl] + final ImageSaverService? _imageSaverService; + + /// A getter method which returns the [ImageSaverService] that is provided + /// by the developer, if it can't be found then we will use default impl + ImageSaverService get imageSaverService { + return _imageSaverService ?? ImageSaverService.defaultImpl(); + } + + /// The property [assetsPrefix] should be the start of your assets folder + /// by default it to `assets` and the reason why we need to know it + /// + /// Because in case when you don't define a value for [ImageProviderBuilder] + /// in the [QuillEditorImageEmbedConfigurations] which exists in + /// [FlutterQuillEmbeds.editorBuilders] + /// + /// then the only way of how to know if this is asset image that you added + /// in the `pubspec.yaml` is by asking you the assetsPrefix, how should the + /// start of your asset images usualy looks like?? in most projects it's + /// assets so we will go with that as a default + /// + /// but if you are using different name and you want to use assets images + /// in the [QuillEditor] then it's important to override this + /// + /// if you want a custom solution then please use [imageProviderBuilder] + final String assetsPrefix; +} diff --git a/flutter_quill_extensions/lib/logic/services/image_picker/image_options.dart b/flutter_quill_extensions/lib/logic/services/image_picker/image_options.dart new file mode 100644 index 00000000..acecbacf --- /dev/null +++ b/flutter_quill_extensions/lib/logic/services/image_picker/image_options.dart @@ -0,0 +1,20 @@ +/// Specifies the source where the picked image should come from. +enum ImageSource { + /// Opens up the device camera, letting the user to take a new picture. + camera, + + /// Opens the user's photo gallery. + gallery, +} + +enum CameraDevice { + /// Use the rear camera. + /// + /// In most of the cases, it is the default configuration. + rear, + + /// Use the front camera. + /// + /// Supported on all iPhones/iPads and some Android devices. + front, +} diff --git a/flutter_quill_extensions/lib/logic/services/image_picker/image_picker.dart b/flutter_quill_extensions/lib/logic/services/image_picker/image_picker.dart new file mode 100644 index 00000000..b79d816a --- /dev/null +++ b/flutter_quill_extensions/lib/logic/services/image_picker/image_picker.dart @@ -0,0 +1,30 @@ +import 'package:cross_file/cross_file.dart' show XFile; + +import 'image_options.dart'; + +export 'package:cross_file/cross_file.dart' show XFile; + +export 'image_options.dart'; + +abstract class ImagePickerInterface { + const ImagePickerInterface(); + Future pickImage({ + required ImageSource source, + double? maxWidth, + double? maxHeight, + int? imageQuality, + CameraDevice preferredCameraDevice = CameraDevice.rear, + bool requestFullMetadata = true, + }); + Future pickMedia({ + double? maxWidth, + double? maxHeight, + int? imageQuality, + bool requestFullMetadata = true, + }); + Future pickVideo({ + required ImageSource source, + CameraDevice preferredCameraDevice = CameraDevice.rear, + Duration? maxDuration, + }); +} diff --git a/flutter_quill_extensions/lib/logic/services/image_picker/packages/image_picker.dart b/flutter_quill_extensions/lib/logic/services/image_picker/packages/image_picker.dart new file mode 100644 index 00000000..e009648d --- /dev/null +++ b/flutter_quill_extensions/lib/logic/services/image_picker/packages/image_picker.dart @@ -0,0 +1,80 @@ +import 'package:image_picker/image_picker.dart' as package + show ImagePicker, ImageSource, CameraDevice; + +import '../image_picker.dart'; + +class ImagePickerPackageImpl extends ImagePickerInterface { + const ImagePickerPackageImpl(); + package.ImagePicker get _picker { + return package.ImagePicker(); + } + + @override + Future pickImage({ + required ImageSource source, + double? maxWidth, + double? maxHeight, + int? imageQuality, + CameraDevice preferredCameraDevice = CameraDevice.rear, + bool requestFullMetadata = true, + }) { + return _picker.pickImage( + source: source.toImagePickerPackage(), + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: imageQuality, + preferredCameraDevice: preferredCameraDevice.toImagePickerPackage(), + requestFullMetadata: requestFullMetadata, + ); + } + + @override + Future pickMedia({ + double? maxWidth, + double? maxHeight, + int? imageQuality, + bool requestFullMetadata = true, + }) { + return _picker.pickMedia( + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: imageQuality, + requestFullMetadata: requestFullMetadata, + ); + } + + @override + Future pickVideo({ + required ImageSource source, + CameraDevice preferredCameraDevice = CameraDevice.rear, + Duration? maxDuration, + }) { + return _picker.pickVideo( + source: source.toImagePickerPackage(), + preferredCameraDevice: preferredCameraDevice.toImagePickerPackage(), + maxDuration: maxDuration, + ); + } +} + +extension ImageSoureceExt on ImageSource { + package.ImageSource toImagePickerPackage() { + switch (this) { + case ImageSource.camera: + return package.ImageSource.camera; + case ImageSource.gallery: + return package.ImageSource.gallery; + } + } +} + +extension CameraDeviceExt on CameraDevice { + package.CameraDevice toImagePickerPackage() { + switch (this) { + case CameraDevice.rear: + return package.CameraDevice.rear; + case CameraDevice.front: + return package.CameraDevice.front; + } + } +} diff --git a/flutter_quill_extensions/lib/logic/services/image_picker/s_image_picker.dart b/flutter_quill_extensions/lib/logic/services/image_picker/s_image_picker.dart new file mode 100644 index 00000000..23d19e2e --- /dev/null +++ b/flutter_quill_extensions/lib/logic/services/image_picker/s_image_picker.dart @@ -0,0 +1,61 @@ +import 'image_picker.dart'; +import 'packages/image_picker.dart'; + +/// A service used for packing images in the extensions package +class ImagePickerService extends ImagePickerInterface { + const ImagePickerService( + this._impl, + ); + + factory ImagePickerService.imagePickerPackage() => const ImagePickerService( + ImagePickerPackageImpl(), + ); + + factory ImagePickerService.defaultImpl() => + ImagePickerService.imagePickerPackage(); + + final ImagePickerInterface _impl; + @override + Future pickImage({ + required ImageSource source, + double? maxWidth, + double? maxHeight, + int? imageQuality, + CameraDevice preferredCameraDevice = CameraDevice.rear, + bool requestFullMetadata = true, + }) => + _impl.pickImage( + source: source, + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: imageQuality, + preferredCameraDevice: preferredCameraDevice, + requestFullMetadata: requestFullMetadata, + ); + + @override + Future pickMedia({ + double? maxWidth, + double? maxHeight, + int? imageQuality, + bool requestFullMetadata = true, + }) => + _impl.pickMedia( + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: imageQuality, + requestFullMetadata: requestFullMetadata, + ); + + @override + Future pickVideo({ + required ImageSource source, + CameraDevice preferredCameraDevice = CameraDevice.rear, + Duration? maxDuration, + }) => + _impl.pickVideo( + source: source, + preferredCameraDevice: preferredCameraDevice, + maxDuration: maxDuration, + ); +} diff --git a/flutter_quill_extensions/lib/logic/services/image_saver/exceptions.dart b/flutter_quill_extensions/lib/logic/services/image_saver/exceptions.dart new file mode 100644 index 00000000..70933e10 --- /dev/null +++ b/flutter_quill_extensions/lib/logic/services/image_saver/exceptions.dart @@ -0,0 +1,23 @@ +import 'package:meta/meta.dart' show immutable; + +enum ImageSaverExceptionType { + accessDenied, + notEnoughSpace, + notSupportedFormat, + unexpected, + unknown; +} + +@immutable +class ImageSaverException implements Exception { + const ImageSaverException({ + required this.message, + required this.type, + }); + + final String message; + final ImageSaverExceptionType type; + + @override + String toString() => 'Error while saving image, error type: ${type.name}'; +} diff --git a/flutter_quill_extensions/lib/logic/services/image_saver/image_saver.dart b/flutter_quill_extensions/lib/logic/services/image_saver/image_saver.dart new file mode 100644 index 00000000..2417ae8c --- /dev/null +++ b/flutter_quill_extensions/lib/logic/services/image_saver/image_saver.dart @@ -0,0 +1,7 @@ +abstract class ImageSaverInterface { + const ImageSaverInterface(); + Future saveLocalImage(String imageUrl); + Future saveImageFromNetwork(Uri imageUrl); + Future hasAccess({required bool toAlbum}); + Future requestAccess({required bool toAlbum}); +} diff --git a/flutter_quill_extensions/lib/logic/services/image_saver/packages/gal.dart b/flutter_quill_extensions/lib/logic/services/image_saver/packages/gal.dart new file mode 100644 index 00000000..aec93ff4 --- /dev/null +++ b/flutter_quill_extensions/lib/logic/services/image_saver/packages/gal.dart @@ -0,0 +1,77 @@ +import 'package:flutter/widgets.dart' show NetworkImageLoadException; +import 'package:gal/gal.dart' show Gal, GalException, GalExceptionType; +import 'package:http/http.dart' as http; + +import '../exceptions.dart'; +import '../image_saver.dart'; + +class ImageSaverGalImpl extends ImageSaverInterface { + @override + Future saveImageFromNetwork(Uri imageUrl) async { + try { + final response = await http.get( + imageUrl, + ); + if (response.statusCode != 200) { + throw NetworkImageLoadException( + statusCode: response.statusCode, + uri: imageUrl, + ); + } + final imageBytes = response.bodyBytes; + await Gal.putImageBytes(imageBytes); + } on GalException catch (e) { + throw ImageSaverException( + message: e.toString(), + type: e.type.toImageSaverExceptionType(), + ); + } catch (e) { + throw ImageSaverException( + message: e.toString(), + type: ImageSaverExceptionType.unknown, + ); + } + } + + @override + Future saveLocalImage(String imageUrl) async { + try { + await Gal.putImage(imageUrl); + } on GalException catch (e) { + throw ImageSaverException( + message: e.toString(), + type: e.type.toImageSaverExceptionType(), + ); + } catch (e) { + throw ImageSaverException( + message: e.toString(), + type: ImageSaverExceptionType.unknown, + ); + } + } + + @override + Future hasAccess({required bool toAlbum}) { + return Gal.hasAccess(toAlbum: toAlbum); + } + + @override + Future requestAccess({required bool toAlbum}) { + return Gal.requestAccess(toAlbum: toAlbum); + } +} + +extension GalExceptionTypeExt on GalExceptionType { + ImageSaverExceptionType toImageSaverExceptionType() { + switch (this) { + case GalExceptionType.accessDenied: + return ImageSaverExceptionType.accessDenied; + case GalExceptionType.notEnoughSpace: + return ImageSaverExceptionType.notEnoughSpace; + case GalExceptionType.notSupportedFormat: + return ImageSaverExceptionType.notSupportedFormat; + case GalExceptionType.unexpected: + return ImageSaverExceptionType.unexpected; + } + } +} diff --git a/flutter_quill_extensions/lib/logic/services/image_saver/s_image_saver.dart b/flutter_quill_extensions/lib/logic/services/image_saver/s_image_saver.dart new file mode 100644 index 00000000..35b9ee34 --- /dev/null +++ b/flutter_quill_extensions/lib/logic/services/image_saver/s_image_saver.dart @@ -0,0 +1,31 @@ +// ignore_for_file: public_member_api_docs, sort_constructors_first +import 'image_saver.dart'; +import 'packages/gal.dart' show ImageSaverGalImpl; + +/// A service used for saving images in the extensions package +class ImageSaverService extends ImageSaverInterface { + final ImageSaverInterface _impl; + const ImageSaverService(this._impl); + + factory ImageSaverService.galPackage() => ImageSaverService( + ImageSaverGalImpl(), + ); + + factory ImageSaverService.defaultImpl() => ImageSaverService.galPackage(); + + @override + Future hasAccess({bool toAlbum = false}) => + _impl.hasAccess(toAlbum: toAlbum); + + @override + Future requestAccess({bool toAlbum = false}) => + _impl.requestAccess(toAlbum: toAlbum); + + @override + Future saveImageFromNetwork(Uri imageUrl) => + _impl.saveImageFromNetwork(imageUrl); + + @override + Future saveLocalImage(String imageUrl) => + _impl.saveLocalImage(imageUrl); +} diff --git a/flutter_quill_extensions/lib/utils/quill_utils.dart b/flutter_quill_extensions/lib/logic/utils/quill_image_utils.dart similarity index 86% rename from flutter_quill_extensions/lib/utils/quill_utils.dart rename to flutter_quill_extensions/lib/logic/utils/quill_image_utils.dart index 1ee9caff..30eca128 100644 --- a/flutter_quill_extensions/lib/utils/quill_utils.dart +++ b/flutter_quill_extensions/lib/logic/utils/quill_image_utils.dart @@ -4,11 +4,21 @@ import 'package:flutter/foundation.dart' show kIsWeb; import 'package:flutter_quill/flutter_quill.dart' as quill; import 'package:path/path.dart' as path; -import '../flutter_quill_extensions.dart'; +import '../../presentation/utils/utils.dart'; + +typedef OnGenerateNewFileNameCallback = String Function( + String currentFileName, + String fileExt, +); class QuillImageUtilities { - const QuillImageUtilities._(); + const QuillImageUtilities({ + required this.controller, + }); + + final quill.QuillController controller; + /// Private function that is throw an error if the platform is web static void _webIsNotSupported(String functionName) { if (kIsWeb) { throw UnsupportedError( @@ -60,7 +70,7 @@ class QuillImageUtilities { required Iterable images, required deleteThePreviousImages, required Directory saveDirectory, - String startOfEachFile = 'quill-image-', + OnGenerateNewFileNameCallback? onGenerateNewFileName, }) async { _webIsNotSupported('saveImagesToDirectory'); final newImagesFutures = images.map((cachedImagePath) async { @@ -71,11 +81,14 @@ class QuillImageUtilities { return ''; } - final newImageFileExtensionWithDot = path.extension(cachedImagePath); + final newImageFileExtension = path.extension(cachedImagePath); // with dot - final dateTimeAsString = DateTime.now().toIso8601String(); - final newImageFileName = - '$startOfEachFile$dateTimeAsString$newImageFileExtensionWithDot'; + final dateTimeString = DateTime.now().toIso8601String(); + final newImageFileName = onGenerateNewFileName?.call( + cachedImagePath, + newImageFileExtension, + ) ?? + 'quill-image-$dateTimeString$newImageFileExtension'; final newImagePath = path.join(saveDirectory.path, newImageFileName); final newImageFile = await previousImageFile.copy(newImagePath); if (deleteThePreviousImages) { @@ -91,6 +104,13 @@ class QuillImageUtilities { /// Deletes all local images referenced in a Quill document. /// it's not supported on web for now /// + /// Be **careful**, on desktop you should never delete user images. only if you + /// are sure the image is saved in applicaton documents directory + /// + /// on mobile the app is sandboxed so you can't delete user images + /// because it will be a copy of the image for the app + /// so you should be safe + /// /// This function removes local images from the /// file system that are referenced in the provided [document]. /// @@ -106,12 +126,9 @@ class QuillImageUtilities { /// print('Error deleting local images: $e'); /// } /// ``` - static Future deleteAllLocalImagesOfDocument( - quill.Document document, - ) async { + Future deleteAllLocalImages() async { _webIsNotSupported('deleteAllLocalImagesOfDocument'); final imagesPaths = getImagesPathsFromDocument( - document, onlyLocalImages: true, ); for (final image in imagesPaths) { @@ -133,7 +150,7 @@ class QuillImageUtilities { /// Retrieves paths to images embedded in a Quill document. /// /// it's not supported on web for now. - /// This function parses the [document] and returns a list of image paths. + /// This function parses the Document and returns a list of image paths. /// /// [document]: The Quill document from which image paths will be retrieved. /// [onlyLocalImages]: If `true`, @@ -151,12 +168,11 @@ class QuillImageUtilities { /// /// Note: This function assumes that images are /// embedded as block embeds in the Quill document. - static Iterable getImagesPathsFromDocument( - quill.Document document, { + Iterable getImagesPathsFromDocument({ required bool onlyLocalImages, }) { _webIsNotSupported('getImagesPathsFromDocument'); - final images = document.root.children + final images = controller.document.root.children .whereType() .where((node) { if (node.isEmpty) { @@ -197,7 +213,6 @@ class QuillImageUtilities { /// Returns `true` if the image is cached, `false` otherwise. /// On other platforms it will always return false static bool isImageCached(String imagePath) { - _webIsNotSupported('isImageCached'); // Determine if the image path is a cached path based on platform if (kIsWeb) { // For now this will not work for web @@ -226,7 +241,6 @@ class QuillImageUtilities { /// It is specifically designed for mobile /// operating systems (Android and iOS). /// - /// [document] is the Quill document from which to extract image paths. /// /// [replaceUnexistentImagesWith] is an optional parameter. /// If provided, it replaces non-existent image paths @@ -235,13 +249,11 @@ class QuillImageUtilities { /// /// Returns a list of cached image paths found in the document. /// On non-mobile platforms, this function returns an empty list. - static Future> getCachedImagePathsFromDocument( - quill.Document document, { + Future> getCachedImagePathsFromDocument({ String? replaceUnexistentImagesWith, }) async { _webIsNotSupported('getCachedImagePathsFromDocument'); final imagePaths = getImagesPathsFromDocument( - document, onlyLocalImages: true, ); diff --git a/flutter_quill_extensions/lib/logic/utils/string.dart b/flutter_quill_extensions/lib/logic/utils/string.dart new file mode 100644 index 00000000..aca39f68 --- /dev/null +++ b/flutter_quill_extensions/lib/logic/utils/string.dart @@ -0,0 +1,38 @@ +import 'package:flutter_quill/flutter_quill.dart' show Attribute; + +import '../extensions/attribute.dart'; + +String replaceStyleStringWithSize( + String cssStyle, { + required double width, + required double height, + required bool isMobile, +}) { + final result = {}; + final pairs = cssStyle.split(';'); + for (final pair in pairs) { + final index = pair.indexOf(':'); + if (index < 0) { + continue; + } + final key = pair.substring(0, index).trim(); + result[key] = pair.substring(index + 1).trim(); + } + + if (isMobile) { + result[AttributeExt.mobileWidth.key] = width.toString(); + result[AttributeExt.mobileHeight.key] = height.toString(); + } else { + result[Attribute.width.key] = width.toString(); + result[Attribute.height.key] = height.toString(); + } + final sb = StringBuffer(); + for (final pair in result.entries) { + sb + ..write(pair.key) + ..write(': ') + ..write(pair.value) + ..write('; '); + } + return sb.toString(); +} diff --git a/flutter_quill_extensions/lib/presentation/embeds/editor/formula.dart b/flutter_quill_extensions/lib/presentation/embeds/editor/formula.dart new file mode 100644 index 00000000..41b61e09 --- /dev/null +++ b/flutter_quill_extensions/lib/presentation/embeds/editor/formula.dart @@ -0,0 +1,45 @@ +import 'package:flutter/widgets.dart'; +import 'package:flutter_quill/extensions.dart' as base; +import 'package:flutter_quill/flutter_quill.dart' + show BlockEmbed, EmbedBuilder, QuillController; + +class QuillEditorFormulaEmbedBuilder extends EmbedBuilder { + const QuillEditorFormulaEmbedBuilder(); + @override + String get key => BlockEmbed.formulaType; + + @override + bool get expanded => false; + + @override + Widget build( + BuildContext context, + QuillController controller, + base.Embed node, + bool readOnly, + bool inline, + TextStyle textStyle, + ) { + throw UnsupportedError( + 'The formula EmbedBuilder is not supported for now.', + ); + // assert(!kIsWeb, 'Please provide formula EmbedBuilder for Web'); + + // final mathController = MathFieldEditingController(); + // return Focus( + // onFocusChange: (hasFocus) { + // if (hasFocus) { + // // If the MathField is tapped, hides the built in keyboard + // SystemChannels.textInput.invokeMethod('TextInput.hide'); + // debugPrint(mathController.currentEditingValue()); + // } + // }, + // child: MathField( + // controller: mathController, + // variables: const ['x', 'y', 'z'], + // onChanged: (value) {}, + // onSubmitted: (value) {}, + // ), + // ); + } +} diff --git a/flutter_quill_extensions/lib/presentation/embeds/editor/image/image.dart b/flutter_quill_extensions/lib/presentation/embeds/editor/image/image.dart new file mode 100644 index 00000000..d718fa0b --- /dev/null +++ b/flutter_quill_extensions/lib/presentation/embeds/editor/image/image.dart @@ -0,0 +1,148 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_quill/extensions.dart' as base; +import 'package:flutter_quill/flutter_quill.dart' hide OptionalSize; + +import '../../../../logic/models/config/shared_configurations.dart'; +import '../../../models/config/editor/image/image.dart'; +import '../../../utils/utils.dart'; +import '../../widgets/image.dart'; +import 'image_menu.dart'; + +class QuillEditorImageEmbedBuilder extends EmbedBuilder { + QuillEditorImageEmbedBuilder({ + required this.configurations, + }); + final QuillEditorImageEmbedConfigurations configurations; + + @override + String get key => BlockEmbed.imageType; + + @override + bool get expanded => false; + + @override + Widget build( + BuildContext context, + QuillController controller, + base.Embed node, + bool readOnly, + bool inline, + TextStyle textStyle, + ) { + assert(!kIsWeb, 'Please provide image EmbedBuilder for Web'); + + final imageSource = standardizeImageUrl(node.value.data); + final ((imageSize), margin, alignment) = getElementAttributes(node); + + final width = imageSize.width; + final height = imageSize.height; + + final image = getImageWidgetByImageSource( + imageSource, + imageProviderBuilder: configurations.imageProviderBuilder, + imageErrorWidgetBuilder: configurations.imageErrorWidgetBuilder, + alignment: alignment, + height: height, + width: width, + assetsPrefix: QuillSharedExtensionsConfigurations.get(context: context) + .assetsPrefix, + ); + + // OptionalSize? imageSize; + // final style = node.style.attributes['style']; + + // if (style != null) { + // final attrs = base.isMobile(supportWeb: false) + // ? base.parseKeyValuePairs(style.value.toString(), { + // Attribute.mobileWidth, + // Attribute.mobileHeight, + // Attribute.mobileMargin, + // Attribute.mobileAlignment, + // }) + // : base.parseKeyValuePairs(style.value.toString(), { + // Attribute.width.key, + // Attribute.height.key, + // Attribute.margin, + // Attribute.alignment, + // }); + // if (attrs.isNotEmpty) { + // final width = double.tryParse( + // (base.isMobile(supportWeb: false) + // ? attrs[Attribute.mobileWidth] + // : attrs[Attribute.width.key]) ?? + // '', + // ); + // final height = double.tryParse( + // (base.isMobile(supportWeb: false) + // ? attrs[Attribute.mobileHeight] + // : attrs[Attribute.height.key]) ?? + // '', + // ); + // final alignment = base.getAlignment(base.isMobile(supportWeb: false) + // ? attrs[Attribute.mobileAlignment] + // : attrs[Attribute.alignment]); + // final margin = (base.isMobile(supportWeb: false) + // ? double.tryParse(Attribute.mobileMargin) + // : double.tryParse(Attribute.margin)) ?? + // 0.0; + + // imageSize = OptionalSize(width, height); + // image = Padding( + // padding: EdgeInsets.all(margin), + // child: getImageWidgetByImageSource( + // imageSource, + // width: width, + // height: height, + // alignment: alignment, + // imageProviderBuilder: configurations.imageProviderBuilder, + // imageErrorWidgetBuilder: configurations.imageErrorWidgetBuilder, + // ), + // ); + // } + // } + + // if (imageSize == null) { + // image = getImageWidgetByImageSource( + // imageSource, + // imageProviderBuilder: configurations.imageProviderBuilder, + // imageErrorWidgetBuilder: configurations.imageErrorWidgetBuilder, + // ); + // imageSize = OptionalSize((image as Image).width, image.height); + // } + + final imageSaverService = + QuillSharedExtensionsConfigurations.get(context: context) + .imageSaverService; + return GestureDetector( + onTap: configurations.onImageClicked ?? + () => showDialog( + context: context, + builder: (_) { + return QuillProvider.value( + value: context.requireQuillProvider, + child: ImageOptionsMenu( + controller: controller, + configurations: configurations, + imageSource: imageSource, + imageSize: imageSize, + isReadOnly: readOnly, + imageSaverService: imageSaverService, + ), + ); + }, + ), + child: Builder( + builder: (context) { + if (margin != null) { + return Padding( + padding: EdgeInsets.all(margin), + child: image, + ); + } + return image; + }, + ), + ); + } +} diff --git a/flutter_quill_extensions/lib/presentation/embeds/editor/image/image_menu.dart b/flutter_quill_extensions/lib/presentation/embeds/editor/image/image_menu.dart new file mode 100644 index 00000000..3b7f34bb --- /dev/null +++ b/flutter_quill_extensions/lib/presentation/embeds/editor/image/image_menu.dart @@ -0,0 +1,195 @@ +import 'package:flutter/cupertino.dart' show showCupertinoModalPopup; +import 'package:flutter/material.dart'; +import 'package:flutter_quill/extensions.dart' show isMobile; +import 'package:flutter_quill/flutter_quill.dart' + show ImageUrl, QuillController, StyleAttribute, getEmbedNode; +import 'package:flutter_quill/translations.dart'; + +import '../../../../logic/models/config/shared_configurations.dart'; +import '../../../../logic/services/image_saver/s_image_saver.dart'; +import '../../../../logic/utils/string.dart'; +import '../../../models/config/editor/image/image.dart'; +import '../../../utils/utils.dart'; +import '../../widgets/image.dart' show ImageTapWrapper, getImageStyleString; +import '../../widgets/image_resizer.dart' show ImageResizer; + +class ImageOptionsMenu extends StatelessWidget { + const ImageOptionsMenu({ + required this.controller, + required this.configurations, + required this.imageSource, + required this.imageSize, + required this.isReadOnly, + required this.imageSaverService, + super.key, + }); + + final QuillController controller; + final QuillEditorImageEmbedConfigurations configurations; + final String imageSource; + final OptionalSize imageSize; + final bool isReadOnly; + final ImageSaverService imageSaverService; + + @override + Widget build(BuildContext context) { + final materialTheme = Theme.of(context); + return Padding( + padding: const EdgeInsets.fromLTRB(50, 0, 50, 0), + child: SimpleDialog( + title: Text(context.loc.image), + children: [ + if (!isReadOnly) + ListTile( + title: Text(context.loc.resize), + leading: const Icon(Icons.settings_outlined), + onTap: () { + Navigator.pop(context); + showCupertinoModalPopup( + context: context, + builder: (context) { + final screenSize = MediaQuery.sizeOf(context); + return ImageResizer( + onImageResize: (width, height) { + final res = getEmbedNode( + controller, + controller.selection.start, + ); + + final attr = replaceStyleStringWithSize( + getImageStyleString(controller), + width: width, + height: height, + isMobile: isMobile(supportWeb: false), + ); + controller + ..skipRequestKeyboard = true + ..formatText( + res.offset, + 1, + StyleAttribute(attr), + ); + }, + imageWidth: imageSize.width, + imageHeight: imageSize.height, + maxWidth: screenSize.width, + maxHeight: screenSize.height, + ); + }, + ); + }, + ), + ListTile( + leading: const Icon(Icons.copy_all_outlined), + title: Text(context.loc.copy), + onTap: () async { + final navigator = Navigator.of(context); + final imageNode = + getEmbedNode(controller, controller.selection.start).value; + final imageUrl = imageNode.value.data; + controller.copiedImageUrl = ImageUrl( + imageUrl, + getImageStyleString(controller), + ); + // TODO: Implement the copy image + // await Clipboard.setData( + // ClipboardData(), + // ); + navigator.pop(); + }, + ), + if (!isReadOnly) + ListTile( + leading: Icon( + Icons.delete_forever_outlined, + color: materialTheme.colorScheme.error, + ), + title: Text(context.loc.remove), + onTap: () async { + Navigator.of(context).pop(); + + // Call the remove check callback if set + if (await configurations.shouldRemoveImageCallback + ?.call(imageSource) == + false) { + return; + } + + final offset = getEmbedNode( + controller, + controller.selection.start, + ).offset; + controller.replaceText( + offset, + 1, + '', + TextSelection.collapsed(offset: offset), + ); + // Call the post remove callback if set + await configurations.onImageRemovedCallback.call(imageSource); + }, + ), + ...[ + ListTile( + leading: const Icon(Icons.save), + title: Text(context.loc.save), + onTap: () async { + final messenger = ScaffoldMessenger.of(context); + final localizations = context.loc; + Navigator.of(context).pop(); + + final saveImageResult = await saveImage( + imageUrl: imageSource, + imageSaverService: imageSaverService, + ); + final imageSavedSuccessfully = saveImageResult.error == null; + + messenger.clearSnackBars(); + + if (!imageSavedSuccessfully) { + messenger.showSnackBar(SnackBar( + content: Text( + localizations.errorWhileSavingImage, + ))); + return; + } + + String message; + switch (saveImageResult.method) { + case SaveImageResultMethod.network: + message = localizations.savedUsingTheNetwork; + break; + case SaveImageResultMethod.localStorage: + message = localizations.savedUsingLocalStorage; + break; + } + + messenger.showSnackBar( + SnackBar( + content: Text(message), + ), + ); + }, + ), + ListTile( + leading: const Icon(Icons.zoom_in), + title: Text(context.loc.zoom), + onTap: () => Navigator.pushReplacement( + context, + MaterialPageRoute( + builder: (_) => ImageTapWrapper( + assetsPrefix: QuillSharedExtensionsConfigurations.get( + context: context) + .assetsPrefix, + imageUrl: imageSource, + configurations: configurations, + ), + ), + ), + ), + ], + ], + ), + ); + } +} diff --git a/flutter_quill_extensions/lib/presentation/embeds/editor/image/image_web.dart b/flutter_quill_extensions/lib/presentation/embeds/editor/image/image_web.dart new file mode 100644 index 00000000..2ed5b967 --- /dev/null +++ b/flutter_quill_extensions/lib/presentation/embeds/editor/image/image_web.dart @@ -0,0 +1,71 @@ +import 'package:flutter/foundation.dart' show kIsWeb; +import 'package:flutter/widgets.dart'; +import 'package:flutter_quill/flutter_quill.dart'; +import 'package:universal_html/html.dart' as html; + +import '../../../models/config/editor/image/image_web.dart'; +import '../../../utils/utils.dart'; +import '../../../utils/web_utils.dart'; +import '../shims/dart_ui_fake.dart' + if (dart.library.html) '../shims/dart_ui_real.dart' as ui; + +class QuillEditorWebImageEmbedBuilder extends EmbedBuilder { + const QuillEditorWebImageEmbedBuilder({ + required this.configurations, + }); + + final QuillEditorWebImageEmbedConfigurations configurations; + + @override + String get key => BlockEmbed.imageType; + + @override + bool get expanded => false; + + @override + Widget build( + BuildContext context, + QuillController controller, + Embed node, + bool readOnly, + bool inline, + TextStyle textStyle, + ) { + assert(kIsWeb, 'ImageEmbedBuilderWeb is only for web platform'); + + final (height, width, margin, alignment) = getWebElementAttributes(node); + + var imageSource = node.value.data.toString(); + + // This logic make sure if the image is imageBase64 then + // it make sure if the pattern is like + // data:image/png;base64, [base64 encoded image string here] + // if not then it will add the data:image/png;base64, at the first + if (isImageBase64(imageSource)) { + // Sometimes the image base 64 for some reasons + // doesn't displayed with the + if (!(imageSource.startsWith('data:image/') && + imageSource.contains('base64'))) { + imageSource = 'data:image/png;base64, $imageSource'; + } + } + + ui.PlatformViewRegistry().registerViewFactory(imageSource, (viewId) { + return html.ImageElement() + ..src = imageSource + ..style.height = height + ..style.width = width + ..style.margin = margin + ..style.alignSelf = alignment + ..attributes['loading'] = 'lazy'; + }); + + return ConstrainedBox( + constraints: configurations.constraints ?? + BoxConstraints.loose(const Size(200, 200)), + child: HtmlElementView( + viewType: imageSource, + ), + ); + } +} diff --git a/flutter_quill_extensions/lib/presentation/embeds/editor/shims/dart_ui_fake.dart b/flutter_quill_extensions/lib/presentation/embeds/editor/shims/dart_ui_fake.dart new file mode 100644 index 00000000..c42e2eb5 --- /dev/null +++ b/flutter_quill_extensions/lib/presentation/embeds/editor/shims/dart_ui_fake.dart @@ -0,0 +1,43 @@ +// import 'package:universal_html/html.dart' as html; + +// Fake interface for the logic that this package needs from (web-only) dart:ui. +// This is conditionally exported so the analyzer sees these methods as +// available. + +// typedef PlatroformViewFactory = html.Element Function(int viewId); + +// /// Shim for web_ui engine.PlatformViewRegistry +// /// https://github.com/flutter/engine/blob/master/lib/web_ui/lib/ui.dart#L62 +// class PlatformViewRegistry { +// /// Shim for registerViewFactory +// /// https://github.com/flutter/engine/blob/master/lib/web_ui/lib/ui.dart#L72 +// static dynamic registerViewFactory( +// String viewTypeId, PlatroformViewFactory viewFactory) {} +// } + +// /// Shim for web_ui engine.AssetManager +// /// https://github.com/flutter/engine/blob/master/lib/web_ui/lib/src/engine/assets.dart#L12 +// class WebOnlyAssetManager { +// static dynamic getAssetUrl(String asset) {} +// } + +class PlatformViewRegistry { + /// Register [viewType] as being created by the given [viewFactory]. + /// + /// [viewFactory] can be any function that takes an integer and optional + /// `params` and returns an `HTMLElement` DOM object. + bool registerViewFactory( + String viewType, + Function viewFactory, { + bool isVisible = true, + }) { + return false; + } + + /// Returns the view previously created for [viewId]. + /// + /// Throws if no view has been created for [viewId]. + Object getViewById(int viewId) { + return ''; + } +} diff --git a/flutter_quill_extensions/lib/shims/dart_ui_real.dart b/flutter_quill_extensions/lib/presentation/embeds/editor/shims/dart_ui_real.dart similarity index 100% rename from flutter_quill_extensions/lib/shims/dart_ui_real.dart rename to flutter_quill_extensions/lib/presentation/embeds/editor/shims/dart_ui_real.dart diff --git a/flutter_quill_extensions/lib/presentation/embeds/editor/unknown.dart b/flutter_quill_extensions/lib/presentation/embeds/editor/unknown.dart new file mode 100644 index 00000000..bac7a6f5 --- /dev/null +++ b/flutter_quill_extensions/lib/presentation/embeds/editor/unknown.dart @@ -0,0 +1,19 @@ +import 'package:flutter/widgets.dart'; +import 'package:flutter_quill/flutter_quill.dart'; + +class QuillEditorUnknownEmbedBuilder extends EmbedBuilder { + @override + Widget build( + BuildContext context, + QuillController controller, + Embed node, + bool readOnly, + bool inline, + TextStyle textStyle, + ) { + return const Text('Unknown embed builder'); + } + + @override + String get key => 'unknown'; +} diff --git a/flutter_quill_extensions/lib/presentation/embeds/editor/video/video.dart b/flutter_quill_extensions/lib/presentation/embeds/editor/video/video.dart new file mode 100644 index 00000000..a4b2a9b8 --- /dev/null +++ b/flutter_quill_extensions/lib/presentation/embeds/editor/video/video.dart @@ -0,0 +1,57 @@ +import 'package:flutter/foundation.dart' show kIsWeb; +import 'package:flutter/material.dart'; +import 'package:flutter_quill/extensions.dart' as base; +import 'package:flutter_quill/flutter_quill.dart'; + +import '../../../models/config/editor/video/video.dart'; +import '../../../utils/utils.dart'; +import '../../widgets/video_app.dart'; +import '../../widgets/youtube_video_app.dart'; + +class QuillEditorVideoEmbedBuilder extends EmbedBuilder { + const QuillEditorVideoEmbedBuilder({ + required this.configurations, + }); + + final QuillEditorVideoEmbedConfigurations configurations; + + @override + String get key => BlockEmbed.videoType; + + @override + Widget build( + BuildContext context, + QuillController controller, + base.Embed node, + bool readOnly, + bool inline, + TextStyle textStyle, + ) { + assert(!kIsWeb, 'Please provide video EmbedBuilder for Web'); + + final videoUrl = node.value.data; + if (isYouTubeUrl(videoUrl)) { + return YoutubeVideoApp( + videoUrl: videoUrl, + context: context, + readOnly: readOnly, + ); + } + final ((elementSize), margin, alignment) = getElementAttributes(node); + + final width = elementSize.width; + final height = elementSize.height; + return Container( + width: width, + height: height, + margin: EdgeInsets.all(margin ?? 0.0), + alignment: alignment, + child: VideoApp( + videoUrl: videoUrl, + context: context, + readOnly: readOnly, + onVideoInit: configurations.onVideoInit, + ), + ); + } +} diff --git a/flutter_quill_extensions/lib/presentation/embeds/editor/video/video_web.dart b/flutter_quill_extensions/lib/presentation/embeds/editor/video/video_web.dart new file mode 100644 index 00000000..abc821d0 --- /dev/null +++ b/flutter_quill_extensions/lib/presentation/embeds/editor/video/video_web.dart @@ -0,0 +1,64 @@ +import 'package:flutter/widgets.dart'; +import 'package:flutter_quill/flutter_quill.dart'; +import 'package:universal_html/html.dart' as html; +import 'package:youtube_player_flutter/youtube_player_flutter.dart' + show YoutubePlayer; + +import '../../../models/config/editor/video/video_web.dart'; +import '../../../utils/utils.dart'; +import '../../../utils/web_utils.dart'; +import '../shims/dart_ui_fake.dart' + if (dart.library.html) '../shims/dart_ui_real.dart' as ui; + +class QuillEditorWebVideoEmbedBuilder extends EmbedBuilder { + const QuillEditorWebVideoEmbedBuilder({ + required this.configurations, + }); + + final QuillEditorWebVideoEmbedConfigurations configurations; + + @override + String get key => BlockEmbed.videoType; + + @override + bool get expanded => false; + + @override + Widget build( + BuildContext context, + QuillController controller, + Embed node, + bool readOnly, + bool inline, + TextStyle textStyle, + ) { + var videoUrl = node.value.data; + if (isYouTubeUrl(videoUrl)) { + final youtubeID = YoutubePlayer.convertUrlToId(videoUrl); + if (youtubeID != null) { + videoUrl = 'https://www.youtube.com/embed/$youtubeID'; + } + } + + final (height, width, margin, alignment) = getWebElementAttributes(node); + + ui.PlatformViewRegistry().registerViewFactory( + videoUrl, + (id) => html.IFrameElement() + ..style.width = width + ..style.height = height + ..src = videoUrl + ..style.border = 'none' + ..style.margin = margin + ..style.alignSelf = alignment + ..attributes['loading'] = 'lazy', + ); + + return SizedBox( + height: 500, + child: HtmlElementView( + viewType: videoUrl, + ), + ); + } +} diff --git a/flutter_quill_extensions/lib/presentation/embeds/editor/webview.dart b/flutter_quill_extensions/lib/presentation/embeds/editor/webview.dart new file mode 100644 index 00000000..4df6cc55 --- /dev/null +++ b/flutter_quill_extensions/lib/presentation/embeds/editor/webview.dart @@ -0,0 +1,58 @@ +import 'dart:convert' show jsonDecode, jsonEncode; + +import 'package:flutter/widgets.dart'; +import 'package:flutter_inappwebview/flutter_inappwebview.dart'; +import 'package:flutter_quill/flutter_quill.dart'; +import 'package:meta/meta.dart' show experimental; + +import '../../models/config/editor/webview.dart'; + +@experimental +class QuillEditorWebViewBlockEmbed extends CustomBlockEmbed { + const QuillEditorWebViewBlockEmbed( + String value, + ) : super(webViewType, value); + + factory QuillEditorWebViewBlockEmbed.fromDocument(Document document) => + QuillEditorWebViewBlockEmbed(jsonEncode(document.toDelta().toJson())); + + static const String webViewType = 'webview'; + + Document get document => Document.fromJson(jsonDecode(data)); +} + +@experimental +class QuillEditorWebViewEmbedBuilder extends EmbedBuilder { + const QuillEditorWebViewEmbedBuilder({ + required this.configurations, + }); + + @override + bool get expanded => false; + + final QuillEditorWebViewEmbedConfigurations configurations; + @override + Widget build( + BuildContext context, + QuillController controller, + Embed node, + bool readOnly, + bool inline, + TextStyle textStyle, + ) { + final url = node.value.data as String; + + return SizedBox( + width: double.infinity, + height: 200, + child: InAppWebView( + initialUrlRequest: URLRequest( + url: Uri.parse(url), + ), + ), + ); + } + + @override + String get key => 'webview'; +} diff --git a/flutter_quill_extensions/lib/presentation/embeds/embed_types.dart b/flutter_quill_extensions/lib/presentation/embeds/embed_types.dart new file mode 100644 index 00000000..249fb40b --- /dev/null +++ b/flutter_quill_extensions/lib/presentation/embeds/embed_types.dart @@ -0,0 +1,12 @@ +import 'package:cross_file/cross_file.dart' show XFile; + +typedef MediaFileUrl = String; +typedef MediaFilePicker = Future Function(QuillMediaType mediaType); +typedef MediaPickedCallback = Future Function(XFile file); + +enum QuillMediaType { image, video } + +extension QuillMediaTypeX on QuillMediaType { + bool get isImage => this == QuillMediaType.image; + bool get isVideo => this == QuillMediaType.video; +} diff --git a/flutter_quill_extensions/lib/presentation/embeds/embed_types/camera.dart b/flutter_quill_extensions/lib/presentation/embeds/embed_types/camera.dart new file mode 100644 index 00000000..1e293123 --- /dev/null +++ b/flutter_quill_extensions/lib/presentation/embeds/embed_types/camera.dart @@ -0,0 +1,48 @@ +import 'package:flutter/widgets.dart' show BuildContext; +import 'package:meta/meta.dart' show immutable; + +import 'image.dart'; +import 'video.dart'; + +enum CameraAction { + video, + image, +} + +/// When the user click the camera button, should we take a photo or record +/// a video using the camera +/// +/// by default will show a dialog that ask the user which option he/she wants +typedef OnRequestCameraActionCallback = Future Function( + BuildContext context, +); + +@immutable +class QuillToolbarCameraConfigurations { + const QuillToolbarCameraConfigurations({ + this.onRequestCameraActionCallback, + OnImageInsertCallback? onImageInsertCallback, + this.onImageInsertedCallback, + this.onVideoInsertedCallback, + OnVideoInsertCallback? onVideoInsertCallback, + }) : _onImageInsertCallback = onImageInsertCallback, + _onVideoInsertCallback = onVideoInsertCallback; + + final OnRequestCameraActionCallback? onRequestCameraActionCallback; + + final OnImageInsertedCallback? onImageInsertedCallback; + + final OnImageInsertCallback? _onImageInsertCallback; + + OnImageInsertCallback get onImageInsertCallback { + return _onImageInsertCallback ?? defaultOnImageInsertCallback(); + } + + final OnVideoInsertedCallback? onVideoInsertedCallback; + + final OnVideoInsertCallback? _onVideoInsertCallback; + + OnVideoInsertCallback get onVideoInsertCallback { + return _onVideoInsertCallback ?? defaultOnVideoInsertCallback(); + } +} diff --git a/flutter_quill_extensions/lib/presentation/embeds/embed_types/image.dart b/flutter_quill_extensions/lib/presentation/embeds/embed_types/image.dart new file mode 100644 index 00000000..c53796ef --- /dev/null +++ b/flutter_quill_extensions/lib/presentation/embeds/embed_types/image.dart @@ -0,0 +1,82 @@ +import 'package:flutter/widgets.dart' + show ImageErrorWidgetBuilder, ImageProvider; +import 'package:flutter/widgets.dart' show BuildContext; +import 'package:flutter_quill/flutter_quill.dart'; +import 'package:meta/meta.dart' show immutable; + +import '../../../logic/extensions/controller.dart'; +import '../../../logic/services/image_picker/s_image_picker.dart'; + +/// When request picking an image, for example when the image button toolbar +/// clicked, it should be null in case the user didn't choose any image or +/// any other reasons, and it should be the image file path as string that is +/// exists in case the user picked the image successfully +/// +/// by default we already have a default implementation that show a dialog +/// request the source for picking the image, from gallery, link or camera +typedef OnRequestPickImage = Future Function( + BuildContext context, + ImagePickerService imagePickerService, +); + +/// A callback will called when inserting a image in the editor +/// it have the logic that will insert the image block using the controller +typedef OnImageInsertCallback = Future Function( + String image, + QuillController controller, +); + +OnImageInsertCallback defaultOnImageInsertCallback() { + return (imageUrl, controller) async { + controller + ..skipRequestKeyboard = true + ..insertImageBlock(imageSource: imageUrl); + }; +} + +/// When a new image picked this callback will called and you might want to +/// do some logic depending on your use case +typedef OnImageInsertedCallback = Future Function( + String image, +); + +enum InsertImageSource { + gallery, + camera, + link, +} + +/// Configurations for dealing with images, on insert a image +/// on request picking a image +@immutable +class QuillToolbarImageConfigurations { + const QuillToolbarImageConfigurations({ + this.onRequestPickImage, + this.onImageInsertedCallback, + OnImageInsertCallback? onImageInsertCallback, + }) : _onImageInsertCallback = onImageInsertCallback; + + final OnRequestPickImage? onRequestPickImage; + + final OnImageInsertedCallback? onImageInsertedCallback; + + final OnImageInsertCallback? _onImageInsertCallback; + + OnImageInsertCallback get onImageInsertCallback { + return _onImageInsertCallback ?? defaultOnImageInsertCallback(); + } +} + +typedef ImageEmbedBuilderWillRemoveCallback = Future Function( + String imageUrl, +); + +typedef ImageEmbedBuilderOnRemovedCallback = Future Function( + String imageUrl, +); + +typedef ImageEmbedBuilderProviderBuilder = ImageProvider Function( + String imageUrl, +); + +typedef ImageEmbedBuilderErrorWidgetBuilder = ImageErrorWidgetBuilder; diff --git a/flutter_quill_extensions/lib/presentation/embeds/embed_types/video.dart b/flutter_quill_extensions/lib/presentation/embeds/embed_types/video.dart new file mode 100644 index 00000000..d69ab5e0 --- /dev/null +++ b/flutter_quill_extensions/lib/presentation/embeds/embed_types/video.dart @@ -0,0 +1,66 @@ +import 'package:flutter/widgets.dart' show BuildContext; +import 'package:flutter_quill/flutter_quill.dart'; +import 'package:meta/meta.dart' show immutable; + +import '../../../logic/extensions/controller.dart'; +import '../../../logic/services/image_picker/s_image_picker.dart'; + +/// When request picking an video, for example when the video button toolbar +/// clicked, it should be null in case the user didn't choose any video or +/// any other reasons, and it should be the video file path as string that is +/// exists in case the user picked the video successfully +/// +/// by default we already have a default implementation that show a dialog +/// request the source for picking the video, from gallery, link or camera +typedef OnRequestPickVideo = Future Function( + BuildContext context, + ImagePickerService imagePickerService, +); + +/// A callback will called when inserting a video in the editor +/// it have the logic that will insert the video block using the controller +typedef OnVideoInsertCallback = Future Function( + String video, + QuillController controller, +); + +OnVideoInsertCallback defaultOnVideoInsertCallback() { + return (videoUrl, controller) async { + controller + ..skipRequestKeyboard = true + ..insertVideoBlock(videoUrl: videoUrl); + }; +} + +/// When a new video picked this callback will called and you might want to +/// do some logic depending on your use case +typedef OnVideoInsertedCallback = Future Function( + String video, +); + +enum InsertVideoSource { + gallery, + camera, + link, +} + +/// Configurations for dealing with videos, on insert a video +/// on request picking a video +@immutable +class QuillToolbarVideoConfigurations { + const QuillToolbarVideoConfigurations({ + this.onRequestPickVideo, + this.onVideoInsertedCallback, + OnVideoInsertCallback? onVideoInsertCallback, + }) : _onVideoInsertCallback = onVideoInsertCallback; + + final OnRequestPickVideo? onRequestPickVideo; + + final OnVideoInsertedCallback? onVideoInsertedCallback; + + final OnVideoInsertCallback? _onVideoInsertCallback; + + OnVideoInsertCallback get onVideoInsertCallback { + return _onVideoInsertCallback ?? defaultOnVideoInsertCallback(); + } +} diff --git a/flutter_quill_extensions/lib/presentation/embeds/toolbar/camera_button/camera_button.dart b/flutter_quill_extensions/lib/presentation/embeds/toolbar/camera_button/camera_button.dart new file mode 100644 index 00000000..937d6443 --- /dev/null +++ b/flutter_quill_extensions/lib/presentation/embeds/toolbar/camera_button/camera_button.dart @@ -0,0 +1,179 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_quill/flutter_quill.dart' + show + QuillController, + QuillIconTheme, + QuillProviderExt, + QuillToolbarBaseButtonOptions, + QuillToolbarIconButton; +import 'package:flutter_quill/translations.dart'; + +import '../../../../logic/models/config/shared_configurations.dart'; +import '../../../../logic/services/image_picker/image_options.dart'; +import '../../../models/config/toolbar/buttons/camera.dart'; +import '../../embed_types/camera.dart'; +import 'select_camera_action.dart'; + +class QuillToolbarCameraButton extends StatelessWidget { + const QuillToolbarCameraButton({ + required this.controller, + required this.options, + super.key, + }); + + final QuillController controller; + final QuillToolbarCameraButtonOptions options; + + double _iconSize(BuildContext context) { + final baseFontSize = baseButtonExtraOptions(context).globalIconSize; + final iconSize = options.iconSize; + return iconSize ?? baseFontSize; + } + + VoidCallback? _afterButtonPressed(BuildContext context) { + return options.afterButtonPressed ?? + baseButtonExtraOptions(context).afterButtonPressed; + } + + QuillIconTheme? _iconTheme(BuildContext context) { + return options.iconTheme ?? baseButtonExtraOptions(context).iconTheme; + } + + QuillToolbarBaseButtonOptions baseButtonExtraOptions(BuildContext context) { + return context.requireQuillToolbarBaseButtonOptions; + } + + IconData _iconData(BuildContext context) { + return options.iconData ?? + baseButtonExtraOptions(context).iconData ?? + Icons.photo_camera; + } + + String _tooltip(BuildContext context) { + return options.tooltip ?? + baseButtonExtraOptions(context).tooltip ?? + context.loc.camera; + } + + void _sharedOnPressed(BuildContext context) { + _onPressedHandler( + context, + controller, + ); + _afterButtonPressed(context); + } + + @override + Widget build(BuildContext context) { + final iconTheme = _iconTheme(context); + final tooltip = _tooltip(context); + final iconSize = _iconSize(context); + final iconData = _iconData(context); + + final childBuilder = + options.childBuilder ?? baseButtonExtraOptions(context).childBuilder; + + if (childBuilder != null) { + childBuilder( + QuillToolbarCameraButtonOptions( + afterButtonPressed: _afterButtonPressed(context), + iconData: options.iconData, + fillColor: options.fillColor, + iconSize: options.iconSize, + iconButtonFactor: options.iconButtonFactor, + iconTheme: options.iconTheme, + tooltip: options.tooltip, + cameraConfigurations: options.cameraConfigurations, + ), + QuillToolbarCameraButtonExtraOptions( + controller: controller, + context: context, + onPressed: () => _sharedOnPressed(context), + ), + ); + } + + final theme = Theme.of(context); + + final iconColor = iconTheme?.iconUnselectedColor ?? theme.iconTheme.color; + final iconFillColor = iconTheme?.iconUnselectedFillColor ?? + (options.fillColor ?? theme.canvasColor); + + return QuillToolbarIconButton( + icon: Icon(iconData, size: iconSize, color: iconColor), + tooltip: tooltip, + highlightElevation: 0, + hoverElevation: 0, + size: iconSize * 1.77, + fillColor: iconFillColor, + borderRadius: iconTheme?.borderRadius ?? 2, + // isDesktop(supportWeb: false) ? null : + onPressed: () => _sharedOnPressed(context), + ); + } + + Future _getCameraAction(BuildContext context) async { + final customCallback = + options.cameraConfigurations.onRequestCameraActionCallback; + if (customCallback != null) { + return await customCallback(context); + } + final cameraAction = await showDialog( + context: context, + builder: (ctx) => const SelectCameraActionDialog(), + ); + + return cameraAction; + } + + Future _onPressedHandler( + BuildContext context, + QuillController controller, + ) async { + final imagePickerService = + QuillSharedExtensionsConfigurations.get(context: context) + .imagePickerService; + + final cameraAction = await _getCameraAction(context); + + if (cameraAction == null) { + return; + } + + switch (cameraAction) { + case CameraAction.video: + final videoFile = await imagePickerService.pickVideo( + source: ImageSource.camera, + ); + if (videoFile == null) { + return; + } + await options.cameraConfigurations.onVideoInsertCallback( + videoFile.path, + controller, + ); + await options.cameraConfigurations.onVideoInsertedCallback + ?.call(videoFile.path); + case CameraAction.image: + final imageFile = await imagePickerService.pickImage( + source: ImageSource.camera, + ); + if (imageFile == null) { + return; + } + await options.cameraConfigurations.onImageInsertCallback( + imageFile.path, + controller, + ); + await options.cameraConfigurations.onImageInsertedCallback + ?.call(imageFile.path); + } + + // final file = await switch (cameraAction) { + // CameraAction.image => + // imagePickerService.pickImage(source: ImageSource.camera), + // CameraAction.video => + // imagePickerService.pickVideo(source: ImageSource.camera), + // }; + } +} diff --git a/flutter_quill_extensions/lib/presentation/embeds/toolbar/camera_button/select_camera_action.dart b/flutter_quill_extensions/lib/presentation/embeds/toolbar/camera_button/select_camera_action.dart new file mode 100644 index 00000000..f8823cf1 --- /dev/null +++ b/flutter_quill_extensions/lib/presentation/embeds/toolbar/camera_button/select_camera_action.dart @@ -0,0 +1,34 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_quill/translations.dart'; + +import '../../embed_types/camera.dart'; + +class SelectCameraActionDialog extends StatelessWidget { + const SelectCameraActionDialog({super.key}); + + @override + Widget build(BuildContext context) { + return AlertDialog( + contentPadding: EdgeInsets.zero, + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TextButton.icon( + icon: const Icon( + Icons.camera, + ), + label: Text(context.loc.photo), + onPressed: () => Navigator.pop(context, CameraAction.image), + ), + TextButton.icon( + icon: const Icon( + Icons.video_call, + ), + label: Text(context.loc.video), + onPressed: () => Navigator.pop(context, CameraAction.video), + ) + ], + ), + ); + } +} diff --git a/flutter_quill_extensions/lib/presentation/embeds/toolbar/formula_button.dart b/flutter_quill_extensions/lib/presentation/embeds/toolbar/formula_button.dart new file mode 100644 index 00000000..e821d689 --- /dev/null +++ b/flutter_quill_extensions/lib/presentation/embeds/toolbar/formula_button.dart @@ -0,0 +1,114 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_quill/flutter_quill.dart'; + +import '../../models/config/toolbar/buttons/formula.dart'; + +class QuillToolbarFormulaButton extends StatelessWidget { + const QuillToolbarFormulaButton({ + required this.controller, + required this.options, + super.key, + }); + + final QuillController controller; + final QuillToolbarFormulaButtonOptions options; + + double _iconSize(BuildContext context) { + final baseFontSize = baseButtonExtraOptions(context).globalIconSize; + final iconSize = options.iconSize; + return iconSize ?? baseFontSize; + } + + double _iconButtonFactor(BuildContext context) { + final baseIconFactor = + baseButtonExtraOptions(context).globalIconButtonFactor; + final iconButtonFactor = options.iconButtonFactor; + return iconButtonFactor ?? baseIconFactor; + } + + VoidCallback? _afterButtonPressed(BuildContext context) { + return options.afterButtonPressed ?? + baseButtonExtraOptions(context).afterButtonPressed; + } + + QuillIconTheme? _iconTheme(BuildContext context) { + return options.iconTheme ?? baseButtonExtraOptions(context).iconTheme; + } + + QuillToolbarBaseButtonOptions baseButtonExtraOptions(BuildContext context) { + return context.requireQuillToolbarBaseButtonOptions; + } + + IconData _iconData(BuildContext context) { + return options.iconData ?? + baseButtonExtraOptions(context).iconData ?? + Icons.functions; + } + + String _tooltip(BuildContext context) { + return options.tooltip ?? + baseButtonExtraOptions(context).tooltip ?? + 'Insert formula'; + // ('Insert formula'.i18n); + } + + void _sharedOnPressed(BuildContext context) { + _onPressedHandler(context); + _afterButtonPressed(context); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + final iconTheme = _iconTheme(context); + + final tooltip = _tooltip(context); + final iconSize = _iconSize(context); + final iconButtonFactor = _iconButtonFactor(context); + final iconData = _iconData(context); + final childBuilder = + options.childBuilder ?? baseButtonExtraOptions(context).childBuilder; + + final iconColor = iconTheme?.iconUnselectedColor ?? theme.iconTheme.color; + final iconFillColor = iconTheme?.iconUnselectedFillColor ?? + (options.fillColor ?? theme.canvasColor); + + if (childBuilder != null) { + return childBuilder( + QuillToolbarFormulaButtonOptions( + afterButtonPressed: _afterButtonPressed(context), + fillColor: iconFillColor, + iconData: iconData, + iconSize: iconSize, + iconButtonFactor: iconButtonFactor, + iconTheme: iconTheme, + tooltip: tooltip, + ), + QuillToolbarFormulaButtonExtraOptions( + context: context, + controller: controller, + onPressed: () => _sharedOnPressed(context), + ), + ); + } + + return QuillToolbarIconButton( + icon: Icon(iconData, size: iconSize, color: iconColor), + tooltip: tooltip, + highlightElevation: 0, + hoverElevation: 0, + size: iconSize * 1.77, + fillColor: iconFillColor, + borderRadius: iconTheme?.borderRadius ?? 2, + onPressed: () => _sharedOnPressed(context), + ); + } + + Future _onPressedHandler(BuildContext context) async { + final index = controller.selection.baseOffset; + final length = controller.selection.extentOffset - index; + + controller.replaceText(index, length, BlockEmbed.formula(''), null); + } +} diff --git a/flutter_quill_extensions/lib/presentation/embeds/toolbar/image_button/image_button.dart b/flutter_quill_extensions/lib/presentation/embeds/toolbar/image_button/image_button.dart new file mode 100644 index 00000000..08c323d2 --- /dev/null +++ b/flutter_quill_extensions/lib/presentation/embeds/toolbar/image_button/image_button.dart @@ -0,0 +1,183 @@ +// ignore_for_file: use_build_context_synchronously + +import 'package:flutter/material.dart'; +import 'package:flutter_quill/flutter_quill.dart'; +import 'package:flutter_quill/translations.dart'; + +import '../../../../logic/models/config/shared_configurations.dart'; +import '../../../../logic/services/image_picker/image_picker.dart'; +import '../../../models/config/toolbar/buttons/image.dart'; +import '../../embed_types/image.dart'; +import '../utils/image_video_utils.dart'; +import 'select_image_source.dart'; + +class QuillToolbarImageButton extends StatelessWidget { + const QuillToolbarImageButton({ + required this.controller, + required this.options, + super.key, + }); + + final QuillController controller; + + final QuillToolbarImageButtonOptions options; + + double _iconSize(BuildContext context) { + final baseFontSize = baseButtonExtraOptions(context).globalIconSize; + final iconSize = options.iconSize; + return iconSize ?? baseFontSize; + } + + double _iconButtonFactor(BuildContext context) { + final baseIconFactor = + baseButtonExtraOptions(context).globalIconButtonFactor; + final iconButtonFactor = options.iconButtonFactor; + return iconButtonFactor ?? baseIconFactor; + } + + VoidCallback? _afterButtonPressed(BuildContext context) { + return options.afterButtonPressed ?? + baseButtonExtraOptions(context).afterButtonPressed; + } + + QuillIconTheme? _iconTheme(BuildContext context) { + return options.iconTheme ?? baseButtonExtraOptions(context).iconTheme; + } + + QuillToolbarBaseButtonOptions baseButtonExtraOptions(BuildContext context) { + return context.requireQuillToolbarBaseButtonOptions; + } + + IconData _iconData(BuildContext context) { + return options.iconData ?? + baseButtonExtraOptions(context).iconData ?? + Icons.image; + } + + String _tooltip(BuildContext context) { + return options.tooltip ?? + baseButtonExtraOptions(context).tooltip ?? + context.loc.insertImage; + } + + void _sharedOnPressed(BuildContext context) { + _onPressedHandler(context); + _afterButtonPressed(context); + } + + @override + Widget build(BuildContext context) { + final tooltip = _tooltip(context); + final iconSize = _iconSize(context); + final iconButtonFactor = _iconButtonFactor(context); + final iconData = _iconData(context); + final childBuilder = + options.childBuilder ?? baseButtonExtraOptions(context).childBuilder; + + if (childBuilder != null) { + return childBuilder( + QuillToolbarImageButtonOptions( + afterButtonPressed: _afterButtonPressed(context), + iconData: iconData, + iconSize: iconSize, + iconButtonFactor: iconButtonFactor, + dialogTheme: options.dialogTheme, + fillColor: options.fillColor, + iconTheme: options.iconTheme, + linkRegExp: options.linkRegExp, + tooltip: options.tooltip, + imageButtonConfigurations: options.imageButtonConfigurations, + ), + QuillToolbarImageButtonExtraOptions( + context: context, + controller: controller, + onPressed: () => _sharedOnPressed(context), + ), + ); + } + + final theme = Theme.of(context); + + final iconTheme = _iconTheme(context); + + final iconColor = iconTheme?.iconUnselectedColor ?? theme.iconTheme.color; + final iconFillColor = iconTheme?.iconUnselectedFillColor ?? + (options.fillColor ?? theme.canvasColor); + + return QuillToolbarIconButton( + icon: Icon( + iconData, + size: iconSize, + color: iconColor, + ), + tooltip: tooltip, + highlightElevation: 0, + hoverElevation: 0, + size: iconSize * 1.77, + fillColor: iconFillColor, + borderRadius: iconTheme?.borderRadius ?? 2, + onPressed: () => _sharedOnPressed(context), + ); + } + + Future _onPressedHandler(BuildContext context) async { + final imagePickerService = + QuillSharedExtensionsConfigurations.get(context: context) + .imagePickerService; + + final onRequestPickImage = + options.imageButtonConfigurations.onRequestPickImage; + if (onRequestPickImage != null) { + final imageUrl = await onRequestPickImage( + context, + imagePickerService, + ); + if (imageUrl != null) { + await options.imageButtonConfigurations + .onImageInsertCallback(imageUrl, controller); + await options.imageButtonConfigurations.onImageInsertedCallback + ?.call(imageUrl); + } + return; + } + final source = await showSelectImageSourceDialog( + context: context, + ); + if (source == null) { + return; + } + + final imageUrl = switch (source) { + InsertImageSource.gallery => (await imagePickerService.pickImage( + source: ImageSource.gallery, + )) + ?.path, + InsertImageSource.link => await _typeLink(context), + InsertImageSource.camera => (await imagePickerService.pickImage( + source: ImageSource.camera, + )) + ?.path, + }; + if (imageUrl == null) { + return; + } + if (imageUrl.trim().isNotEmpty) { + await options.imageButtonConfigurations + .onImageInsertCallback(imageUrl, controller); + await options.imageButtonConfigurations.onImageInsertedCallback + ?.call(imageUrl); + } + } + + Future _typeLink(BuildContext context) async { + final value = await showDialog( + context: context, + builder: (_) => TypeLinkDialog( + dialogTheme: options.dialogTheme, + linkRegExp: options.linkRegExp, + linkType: LinkType.image, + ), + ); + return value; + } +} diff --git a/flutter_quill_extensions/lib/presentation/embeds/toolbar/image_button/select_image_source.dart b/flutter_quill_extensions/lib/presentation/embeds/toolbar/image_button/select_image_source.dart new file mode 100644 index 00000000..5d1cf4b2 --- /dev/null +++ b/flutter_quill_extensions/lib/presentation/embeds/toolbar/image_button/select_image_source.dart @@ -0,0 +1,59 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_quill/extensions.dart' show isDesktop; + +import '../../embed_types/image.dart'; + +class SelectImageSourceDialog extends StatelessWidget { + const SelectImageSourceDialog({super.key}); + + @override + Widget build(BuildContext context) { + return SizedBox( + height: 230, + width: double.infinity, + child: SingleChildScrollView( + child: Column( + children: [ + ListTile( + title: const Text('Gallery'), + subtitle: const Text( + 'Pick a photo from your gallery', + ), + leading: const Icon(Icons.photo_sharp), + onTap: () => Navigator.of(context).pop(InsertImageSource.gallery), + ), + ListTile( + title: const Text('Camera'), + subtitle: const Text( + 'Take a photo using your phone camera', + ), + leading: const Icon(Icons.camera), + enabled: !isDesktop(supportWeb: false), + onTap: () => Navigator.of(context).pop(InsertImageSource.camera), + ), + ListTile( + title: const Text('Link'), + subtitle: const Text( + 'Paste a photo using a link', + ), + leading: const Icon(Icons.link), + onTap: () => Navigator.of(context).pop(InsertImageSource.link), + ), + ], + ), + ), + ); + } +} + +Future showSelectImageSourceDialog({ + required BuildContext context, +}) async { + final imageSource = await showModalBottomSheet( + showDragHandle: true, + context: context, + constraints: const BoxConstraints(maxWidth: 640), + builder: (context) => const SelectImageSourceDialog(), + ); + return imageSource; +} diff --git a/flutter_quill_extensions/lib/presentation/embeds/toolbar/media_button/media_button.dart b/flutter_quill_extensions/lib/presentation/embeds/toolbar/media_button/media_button.dart new file mode 100644 index 00000000..5246e838 --- /dev/null +++ b/flutter_quill_extensions/lib/presentation/embeds/toolbar/media_button/media_button.dart @@ -0,0 +1,537 @@ +// // ignore_for_file: use_build_context_synchronously + +// import 'dart:math' as math; +// import 'dart:ui'; + +// 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/translations.dart'; +// import 'package:image_picker/image_picker.dart'; + +// import '../../../models/config/toolbar/buttons/media_button.dart'; +// import '../../embed_types.dart'; +// import '../utils/image_video_utils.dart'; + +// /// Widget which combines [ImageButton] and [VideButton] widgets. This widget +// /// has more customization and uses dialog similar to one which is used +// /// on [http://quilljs.com]. +// class QuillToolbarMediaButton extends StatelessWidget { +// QuillToolbarMediaButton({ +// required this.controller, +// required this.options, +// super.key, +// }) : assert(options.type == QuillMediaType.image, +// 'Video selection is not supported yet'); + +// final QuillController controller; +// final QuillToolbarMediaButtonOptions options; + +// double _iconSize(BuildContext context) { +// final baseFontSize = baseButtonExtraOptions(context).globalIconSize; +// final iconSize = options.iconSize; +// return iconSize ?? baseFontSize; +// } + +// VoidCallback? _afterButtonPressed(BuildContext context) { +// return options.afterButtonPressed ?? +// baseButtonExtraOptions(context).afterButtonPressed; +// } + +// QuillIconTheme? _iconTheme(BuildContext context) { +// return options.iconTheme ?? baseButtonExtraOptions(context).iconTheme; +// } + +// QuillToolbarBaseButtonOptions baseButtonExtraOptions( +//BuildContext context) { +// return context.requireQuillToolbarBaseButtonOptions; +// } + +// (IconData, String) get _defaultData { +// switch (options.type) { +// case QuillMediaType.image: +// return (Icons.perm_media, 'Photo media button'); +// case QuillMediaType.video: +// throw UnsupportedError('The video is not supported yet.'); +// } +// } + +// IconData _iconData(BuildContext context) { +// return options.iconData ?? +// baseButtonExtraOptions(context).iconData ?? +// _defaultData.$1; +// } + +// String _tooltip(BuildContext context) { +// return options.tooltip ?? +// baseButtonExtraOptions(context).tooltip ?? +// _defaultData.$2; +// // ('Camera'.i18n); +// } + +// void _sharedOnPressed(BuildContext context) { +// _onPressedHandler(context); +// _afterButtonPressed(context); +// } + +// @override +// Widget build(BuildContext context) { +// final tooltip = _tooltip(context); +// final iconSize = _iconSize(context); +// final iconData = _iconData(context); +// final childBuilder = +// options.childBuilder ?? baseButtonExtraOptions(context).childBuilder; +// final iconTheme = _iconTheme(context); + +// if (childBuilder != null) { +// return childBuilder( +// QuillToolbarMediaButtonOptions( +// type: options.type, +// onMediaPickedCallback: options.onMediaPickedCallback, +// onImagePickCallback: options.onImagePickCallback, +// onVideoPickCallback: options.onVideoPickCallback, +// iconData: iconData, +// afterButtonPressed: _afterButtonPressed(context), +// autovalidateMode: options.autovalidateMode, +// childrenSpacing: options.childrenSpacing, +// dialogBarrierColor: options.dialogBarrierColor, +// dialogTheme: options.dialogTheme, +// filePickImpl: options.filePickImpl, +// fillColor: options.fillColor, +// galleryButtonText: options.galleryButtonText, +// iconTheme: iconTheme, +// iconSize: iconSize, +// iconButtonFactor: iconButtonFactor, +// hintText: options.hintText, +// labelText: options.labelText, +// submitButtonSize: options.submitButtonSize, +// linkButtonText: options.linkButtonText, +// mediaFilePicker: options.mediaFilePicker, +// submitButtonText: options.submitButtonText, +// validationMessage: options.validationMessage, +// webImagePickImpl: options.webImagePickImpl, +// webVideoPickImpl: options.webVideoPickImpl, +// tooltip: options.tooltip, +// ), +// QuillToolbarMediaButtonExtraOptions( +// context: context, +// controller: controller, +// onPressed: () => _sharedOnPressed(context), +// ), +// ); +// } + +// final theme = Theme.of(context); + +// final iconColor = +// options.iconTheme?.iconUnselectedColor ?? theme.iconTheme.color; +// final iconFillColor = options.iconTheme?.iconUnselectedFillColor ?? +// options.fillColor ?? +// theme.canvasColor; + +// return QuillToolbarIconButton( +// icon: Icon(iconData, size: iconSize, color: iconColor), +// tooltip: tooltip, +// highlightElevation: 0, +// hoverElevation: 0, +// size: iconSize * 1.77, +// fillColor: iconFillColor, +// borderRadius: iconTheme?.borderRadius ?? 2, +// onPressed: () => _sharedOnPressed(context), +// ); +// } + +// Future _onPressedHandler(BuildContext context) async { +// if (options.onMediaPickedCallback == null) { +// _inputLink(context); +// return; +// } +// final mediaSource = await showDialog( +// context: context, +// builder: (_) => MediaSourceSelectorDialog( +// dialogTheme: options.dialogTheme, +// galleryButtonText: options.galleryButtonText, +// linkButtonText: options.linkButtonText, +// ), +// ); +// if (mediaSource == null) { +// return; +// } +// switch (mediaSource) { +// case MediaPickSetting.gallery: +// await _pickImage(); +// break; +// case MediaPickSetting.link: +// _inputLink(context); +// break; +// case MediaPickSetting.camera: +// await ImageVideoUtils.handleImageButtonTap( +// context, +// controller, +// ImageSource.camera, +// options.onImagePickCallback, +// filePickImpl: options.filePickImpl, +// webImagePickImpl: options.webImagePickImpl, +// ); +// break; +// case MediaPickSetting.video: +// await ImageVideoUtils.handleVideoButtonTap( +// context, +// controller, +// ImageSource.camera, +// options.onVideoPickCallback, +// filePickImpl: options.filePickImpl, +// webVideoPickImpl: options.webVideoPickImpl, +// ); +// break; +// } +// } + +// Future _pickImage() async { +// if (!(kIsWeb || isMobile() || isDesktop())) { +// throw UnsupportedError( +// 'Unsupported target platform: ${defaultTargetPlatform.name}', +// ); +// } + +// final mediaFileUrl = await _pickMediaFileUrl(); + +// if (mediaFileUrl != null) { +// final index = controller.selection.baseOffset; +// final length = controller.selection.extentOffset - index; +// controller.replaceText( +// index, +// length, +// BlockEmbed.image(mediaFileUrl), +// null, +// ); +// } +// } + +// Future _pickMediaFileUrl() async { +// final mediaFile = await options.mediaFilePicker?.call(options.type); +// return mediaFile != null +// ? options.onMediaPickedCallback?.call(mediaFile) +// : null; +// } + +// void _inputLink(BuildContext context) { +// showDialog( +// context: context, +// builder: (_) => MediaLinkDialog( +// dialogTheme: options.dialogTheme, +// labelText: options.labelText, +// hintText: options.hintText, +// buttonText: options.submitButtonText, +// buttonSize: options.submitButtonSize, +// childrenSpacing: options.childrenSpacing, +// autovalidateMode: options.autovalidateMode, +// validationMessage: options.validationMessage, +// ), +// ).then(_linkSubmitted); +// } + +// void _linkSubmitted(String? value) { +// if (value != null && value.isNotEmpty) { +// final index = controller.selection.baseOffset; +// final length = controller.selection.extentOffset - index; +// final data = options.type.isImage +// ? BlockEmbed.image(value) +// : BlockEmbed.video(value); +// controller.replaceText(index, length, data, null); +// } +// } +// } + +// /// Provides a dialog for input link to media resource. +// class MediaLinkDialog extends StatefulWidget { +// const MediaLinkDialog({ +// super.key, +// this.link, +// this.dialogTheme, +// this.childrenSpacing = 16.0, +// this.labelText, +// this.hintText, +// this.buttonText, +// this.buttonSize, +// this.autovalidateMode = AutovalidateMode.disabled, +// this.validationMessage, +// }) : assert(childrenSpacing > 0); + +// final String? link; +// final QuillDialogTheme? dialogTheme; + +// /// The margin between child widgets in the dialog. +// final double childrenSpacing; + +// /// The text of label in link add mode. +// final String? labelText; + +// /// The hint text for link [TextField]. +// final String? hintText; + +// /// The text of the submit button. +// final String? buttonText; + +// /// The size of dialog buttons. +// final Size? buttonSize; + +// final AutovalidateMode autovalidateMode; +// final String? validationMessage; + +// @override +// State createState() => _MediaLinkDialogState(); +// } + +// class _MediaLinkDialogState extends State { +// final _linkFocus = FocusNode(); +// final _linkController = TextEditingController(); + +// @override +// void dispose() { +// _linkFocus.dispose(); +// _linkController.dispose(); +// super.dispose(); +// } + +// @override +// Widget build(BuildContext context) { +// final constraints = widget.dialogTheme?.linkDialogConstraints ?? +// () { +// final size = MediaQuery.sizeOf(context); +// final maxWidth = kIsWeb ? size.width / 4 : size.width - 80; +// return BoxConstraints(maxWidth: maxWidth, maxHeight: 80); +// }(); + +// final buttonStyle = widget.buttonSize != null +// ? Theme.of(context) +// .elevatedButtonTheme +// .style +// ?.copyWith( +//fixedSize: MaterialStatePropertyAll(widget.buttonSize)) +// : widget.dialogTheme?.buttonStyle; + +// final isWrappable = widget.dialogTheme?.isWrappable ?? false; + +// final children = [ +// Text(widget.labelText ?? 'Enter media'.i18n), +// UtilityWidgets.maybeWidget( +// enabled: !isWrappable, +// wrapper: (child) => Expanded( +// child: child, +// ), +// child: Padding( +// padding: EdgeInsets.symmetric(horizontal: widget.childrenSpacing), +// child: TextFormField( +// controller: _linkController, +// focusNode: _linkFocus, +// style: widget.dialogTheme?.inputTextStyle, +// keyboardType: TextInputType.url, +// textInputAction: TextInputAction.done, +// decoration: InputDecoration( +// labelStyle: widget.dialogTheme?.labelTextStyle, +// hintText: widget.hintText, +// ), +// autofocus: true, +// autovalidateMode: widget.autovalidateMode, +// validator: _validateLink, +// onChanged: _linkChanged, +// ), +// ), +// ), +// ElevatedButton( +// onPressed: _canPress() ? _submitLink : null, +// style: buttonStyle, +// child: Text(widget.buttonText ?? 'Ok'.i18n), +// ), +// ]; + +// return Dialog( +// backgroundColor: widget.dialogTheme?.dialogBackgroundColor, +// shape: widget.dialogTheme?.shape ?? +// DialogTheme.of(context).shape ?? +// RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)), +// child: ConstrainedBox( +// constraints: constraints, +// child: Padding( +// padding: +// widget.dialogTheme?.linkDialogPadding ?? const +// EdgeInsets.all(16), +// child: Form( +// child: isWrappable +// ? Wrap( +// alignment: WrapAlignment.center, +// crossAxisAlignment: WrapCrossAlignment.center, +// runSpacing: widget.dialogTheme?.runSpacing ?? 0.0, +// children: children, +// ) +// : Row( +// children: children, +// ), +// ), +// ), +// ), +// ); +// } + +// bool _canPress() => _validateLink(_linkController.text) == null; + +// void _linkChanged(String value) { +// setState(() { +// _linkController.text = value; +// }); +// } + +// void _submitLink() => Navigator.pop(context, _linkController.text); + +// String? _validateLink(String? value) { +// if ((value?.isEmpty ?? false) || +// !AutoFormatMultipleLinksRule.oneLineLinkRegExp.hasMatch(value!)) { +// return widget.validationMessage ?? 'That is not a valid URL'; +// } + +// return null; +// } +// } + +// /// Media souce selector. +// class MediaSourceSelectorDialog extends StatelessWidget { +// const MediaSourceSelectorDialog({ +// super.key, +// this.dialogTheme, +// this.galleryButtonText, +// this.linkButtonText, +// }); + +// final QuillDialogTheme? dialogTheme; + +// /// The text of the gallery button [MediaSourceSelectorDialog]. +// final String? galleryButtonText; + +// /// The text of the link button [MediaSourceSelectorDialog]. +// final String? linkButtonText; + +// @override +// Widget build(BuildContext context) { +// final constraints = dialogTheme?.mediaSelectorDialogConstraints ?? +// () { +// final size = MediaQuery.sizeOf(context); +// double maxWidth, maxHeight; +// if (kIsWeb) { +// maxWidth = size.width / 7; +// maxHeight = size.height / 7; +// } else { +// maxWidth = size.width - 80; +// maxHeight = maxWidth / 2; +// } +// return BoxConstraints(maxWidth: maxWidth, maxHeight: maxHeight); +// }(); + +// final shape = dialogTheme?.shape ?? +// DialogTheme.of(context).shape ?? +// RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)); + +// return Dialog( +// backgroundColor: dialogTheme?.dialogBackgroundColor, +// shape: shape, +// child: ConstrainedBox( +// constraints: constraints, +// child: Padding( +// padding: dialogTheme?.mediaSelectorDialogPadding ?? +// const EdgeInsets.all(16), +// child: Row( +// mainAxisAlignment: MainAxisAlignment.spaceBetween, +// children: [ +// Expanded( +// child: TextButtonWithIcon( +// icon: Icons.collections, +// label: galleryButtonText ?? 'Gallery'.i18n, +// onPressed: () => +// Navigator.pop(context, MediaPickSetting.gallery), +// ), +// ), +// const SizedBox(width: 10), +// Expanded( +// child: TextButtonWithIcon( +// icon: Icons.link, +// label: linkButtonText ?? 'Link'.i18n, +// onPressed: () => +// Navigator.pop(context, MediaPickSetting.link), +// ), +// ) +// ], +// ), +// ), +// ), +// ); +// } +// } + +// class TextButtonWithIcon extends StatelessWidget { +// const TextButtonWithIcon({ +// required this.label, +// required this.icon, +// required this.onPressed, +// this.textStyle, +// super.key, +// }); + +// final String label; +// final IconData icon; +// final VoidCallback onPressed; +// final TextStyle? textStyle; + +// @override +// Widget build(BuildContext context) { +// final theme = Theme.of(context); +// final scale = MediaQuery.maybeOf(context)?.textScaleFactor ?? 1; +// final gap = scale <= 1 ? 8.0 : lerpDouble(8, 4, math.min(scale - 1, 1))!; +// final buttonStyle = TextButtonTheme.of(context).style; +// final shape = buttonStyle?.shape?.resolve({}) ?? +// const RoundedRectangleBorder( +// borderRadius: BorderRadius.all( +// Radius.circular(4), +// ), +// ); +// return Material( +// shape: shape, +// textStyle: textStyle ?? +// theme.textButtonTheme.style?.textStyle?.resolve({}) ?? +// theme.textTheme.labelLarge, +// elevation: buttonStyle?.elevation?.resolve({}) ?? 0, +// child: InkWell( +// customBorder: shape, +// onTap: onPressed, +// child: Padding( +// padding: const EdgeInsets.all(16), +// child: Column( +// mainAxisSize: MainAxisSize.min, +// children: [ +// Icon(icon), +// SizedBox(height: gap), +// Flexible(child: Text(label)), +// ], +// ), +// ), +// ), +// ); +// } +// } + +// /// Default file picker. +// // Future _defaultMediaPicker(QuillMediaType mediaType) async { +// // final pickedFile = mediaType.isImage +// // ? await ImagePicker().pickImage(source: ImageSource.gallery) +// // : await ImagePicker().pickVideo(source: ImageSource.gallery); + +// // if (pickedFile != null) { +// // return QuillFile( +// // name: pickedFile.name, +// // path: pickedFile.path, +// // bytes: await pickedFile.readAsBytes(), +// // ); +// // } + +// // return null; +// // } diff --git a/flutter_quill_extensions/lib/presentation/embeds/toolbar/utils/image_video_utils.dart b/flutter_quill_extensions/lib/presentation/embeds/toolbar/utils/image_video_utils.dart new file mode 100644 index 00000000..03514f1b --- /dev/null +++ b/flutter_quill_extensions/lib/presentation/embeds/toolbar/utils/image_video_utils.dart @@ -0,0 +1,263 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_quill/flutter_quill.dart' show QuillDialogTheme; +import 'package:flutter_quill/translations.dart'; + +enum LinkType { + video, + image, +} + +class TypeLinkDialog extends StatefulWidget { + const TypeLinkDialog({ + required this.linkType, + this.dialogTheme, + this.link, + this.linkRegExp, + super.key, + }); + + final QuillDialogTheme? dialogTheme; + final String? link; + final RegExp? linkRegExp; + final LinkType linkType; + + @override + TypeLinkDialogState createState() => TypeLinkDialogState(); +} + +class TypeLinkDialogState extends State { + late String _link; + late TextEditingController _controller; + late RegExp _linkRegExp; + + @override + void initState() { + super.initState(); + _link = widget.link ?? ''; + _controller = TextEditingController(text: _link); + + final defaultLinkNonSecureRegExp = RegExp( + r'https?://.*?\.(?:png|jpe?g|gif|bmp|webp|tiff?)', + caseSensitive: false, + ); // Not secure + // final defaultLinkRegExp = RegExp( + // r'https://.*?\.(?:png|jpe?g|gif|bmp|webp|tiff?)', + // caseSensitive: false, + // ); // Secure + _linkRegExp = widget.linkRegExp ?? defaultLinkNonSecureRegExp; + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return AlertDialog( + backgroundColor: widget.dialogTheme?.dialogBackgroundColor, + content: TextField( + keyboardType: TextInputType.url, + textInputAction: TextInputAction.done, + maxLines: null, + style: widget.dialogTheme?.inputTextStyle, + decoration: InputDecoration( + labelText: context.loc.pasteLink, + hintText: widget.linkType == LinkType.image + ? context.loc.pleaseEnterAValidImageURL + : context.loc.pleaseEnterAValidVideoURL, + labelStyle: widget.dialogTheme?.labelTextStyle, + floatingLabelStyle: widget.dialogTheme?.labelTextStyle, + ), + autofocus: true, + onChanged: _linkChanged, + controller: _controller, + onEditingComplete: () { + if (!_canPress()) { + return; + } + _applyLink(); + }, + ), + actions: [ + TextButton( + onPressed: _canPress() ? _applyLink : null, + child: Text( + context.loc.ok, + style: widget.dialogTheme?.labelTextStyle, + ), + ), + ], + ); + } + + void _linkChanged(String value) { + setState(() { + _link = value; + }); + } + + void _applyLink() { + Navigator.pop(context, _link.trim()); + } + + bool _canPress() { + return _link.isNotEmpty && _linkRegExp.hasMatch(_link); + } +} + +// @immutable +// class ImageVideoUtils { +// const ImageVideoUtils._(); +// static Future selectMediaPickSetting( +// BuildContext context, +// ) => +// showDialog( +// context: context, +// builder: (ctx) => AlertDialog( +// contentPadding: EdgeInsets.zero, +// content: Column( +// mainAxisSize: MainAxisSize.min, +// children: [ +// TextButton.icon( +// icon: const Icon( +// Icons.collections, +// color: Colors.orangeAccent, +// ), +// label: Text('Gallery'.i18n), +// onPressed: () => Navigator.pop(ctx, +// MediaPickSetting.gallery), +// ), +// TextButton.icon( +// icon: const Icon( +// Icons.link, +// color: Colors.cyanAccent, +// ), +// label: Text('Link'.i18n), +// onPressed: () => Navigator.pop(ctx, MediaPickSetting.link), +// ) +// ], +// ), +// ), +// ); + +// /// For image picking logic +// static Future handleImageButtonTap( +// BuildContext context, +// QuillController controller, +// ImageSource imageSource, +// OnImagePickCallback onImagePickCallback, { +// FilePickImpl? filePickImpl, +// WebImagePickImpl? webImagePickImpl, +// }) async { +// String? imageUrl; +// if (kIsWeb) { +// if (webImagePickImpl != null) { +// imageUrl = await webImagePickImpl(onImagePickCallback); +// return; +// } +// final file = await ImagePicker() +//.pickImage(source: ImageSource.gallery); +// imageUrl = file?.path; +// if (imageUrl == null) { +// return; +// } +// } else if (isMobile()) { +// imageUrl = await _pickImage(imageSource, onImagePickCallback); +// } else { +// assert(filePickImpl != null, 'Desktop must provide filePickImpl'); +// imageUrl = +// await _pickImageDesktop +//(context, filePickImpl!, onImagePickCallback); +// } + +// if (imageUrl == null) { +// return; +// } + +// controller.insertImageBlock( +// imageUrl: imageUrl, +// ); +// } + +// static Future _pickImage( +// ImageSource source, +// OnImagePickCallback onImagePickCallback, +// ) async { +// final pickedFile = await ImagePicker().pickImage(source: source); +// if (pickedFile == null) { +// return null; +// } + +// return onImagePickCallback(File(pickedFile.path)); +// } + +// static Future _pickImageDesktop( +// BuildContext context, +// FilePickImpl filePickImpl, +// OnImagePickCallback onImagePickCallback, +// ) async { +// final filePath = await filePickImpl(context); +// if (filePath == null || filePath.isEmpty) return null; + +// final file = File(filePath); +// return onImagePickCallback(file); +// } + +// /// For video picking logic +// static Future handleVideoButtonTap( +// BuildContext context, +// QuillController controller, +// ImageSource videoSource, +// OnVideoPickCallback onVideoPickCallback, { +// FilePickImpl? filePickImpl, +// WebVideoPickImpl? webVideoPickImpl, +// }) async { +// final index = controller.selection.baseOffset; +// final length = controller.selection.extentOffset - index; + +// String? videoUrl; +// if (kIsWeb) { +// assert( +// webVideoPickImpl != null, +// 'Please provide webVideoPickImpl for Web ' +// 'in the options of this button', +// ); +// videoUrl = await webVideoPickImpl!(onVideoPickCallback); +// } else if (isMobile()) { +// videoUrl = await _pickVideo(videoSource, onVideoPickCallback); +// } else { +// assert(filePickImpl != null, 'Desktop must provide filePickImpl'); +// videoUrl = +// await _pickVideoDesktop(context, filePickImpl!, +// onVideoPickCallback); +// } + +// if (videoUrl != null) { +// controller.replaceText(index, length, BlockEmbed.video(videoUrl), +// null); +// } +// } + +// static Future _pickVideo( +// ImageSource source, OnVideoPickCallback onVideoPickCallback) async { +// final pickedFile = await ImagePicker().pickVideo(source: source); +// if (pickedFile == null) { +// return null; +// } + +// return onVideoPickCallback(File(pickedFile.path)); +// } + +// static Future _pickVideoDesktop( +// BuildContext context, +// FilePickImpl filePickImpl, +// OnVideoPickCallback onVideoPickCallback) async { +// final filePath = await filePickImpl(context); +// if (filePath == null || filePath.isEmpty) return null; + +// final file = File(filePath); +// return onVideoPickCallback(file); +// } +// } diff --git a/flutter_quill_extensions/lib/presentation/embeds/toolbar/video_button/select_video_source.dart b/flutter_quill_extensions/lib/presentation/embeds/toolbar/video_button/select_video_source.dart new file mode 100644 index 00000000..8e2d34d5 --- /dev/null +++ b/flutter_quill_extensions/lib/presentation/embeds/toolbar/video_button/select_video_source.dart @@ -0,0 +1,59 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_quill/extensions.dart' show isDesktop; + +import '../../embed_types/video.dart'; + +class SelectVideoSourceDialog extends StatelessWidget { + const SelectVideoSourceDialog({super.key}); + + @override + Widget build(BuildContext context) { + return SizedBox( + height: 230, + width: double.infinity, + child: SingleChildScrollView( + child: Column( + children: [ + ListTile( + title: const Text('Gallery'), + subtitle: const Text( + 'Pick a video from your gallery', + ), + leading: const Icon(Icons.photo_sharp), + onTap: () => Navigator.of(context).pop(InsertVideoSource.gallery), + ), + ListTile( + title: const Text('Camera'), + subtitle: const Text( + 'Record a video using your phone camera', + ), + leading: const Icon(Icons.camera), + enabled: !isDesktop(supportWeb: false), + onTap: () => Navigator.of(context).pop(InsertVideoSource.camera), + ), + ListTile( + title: const Text('Link'), + subtitle: const Text( + 'Paste a video using a link', + ), + leading: const Icon(Icons.link), + onTap: () => Navigator.of(context).pop(InsertVideoSource.link), + ), + ], + ), + ), + ); + } +} + +Future showSelectVideoSourceDialog({ + required BuildContext context, +}) async { + final imageSource = await showModalBottomSheet( + showDragHandle: true, + context: context, + constraints: const BoxConstraints(maxWidth: 640), + builder: (context) => const SelectVideoSourceDialog(), + ); + return imageSource; +} diff --git a/flutter_quill_extensions/lib/presentation/embeds/toolbar/video_button/video_button.dart b/flutter_quill_extensions/lib/presentation/embeds/toolbar/video_button/video_button.dart new file mode 100644 index 00000000..32b660f4 --- /dev/null +++ b/flutter_quill_extensions/lib/presentation/embeds/toolbar/video_button/video_button.dart @@ -0,0 +1,183 @@ +// ignore_for_file: use_build_context_synchronously + +import 'package:flutter/material.dart'; +import 'package:flutter_quill/flutter_quill.dart'; + +import '../../../../logic/models/config/shared_configurations.dart'; +import '../../../../logic/services/image_picker/image_options.dart'; +import '../../../models/config/toolbar/buttons/video.dart'; +import '../../embed_types/video.dart'; +import '../utils/image_video_utils.dart'; +import 'select_video_source.dart'; + +class QuillToolbarVideoButton extends StatelessWidget { + const QuillToolbarVideoButton({ + required this.options, + required this.controller, + super.key, + }); + + final QuillController controller; + + final QuillToolbarVideoButtonOptions options; + + double _iconSize(BuildContext context) { + final baseFontSize = baseButtonExtraOptions(context).globalIconSize; + final iconSize = options.iconSize; + return iconSize ?? baseFontSize; + } + + VoidCallback? _afterButtonPressed(BuildContext context) { + return options.afterButtonPressed ?? + baseButtonExtraOptions(context).afterButtonPressed; + } + + QuillIconTheme? _iconTheme(BuildContext context) { + return options.iconTheme ?? baseButtonExtraOptions(context).iconTheme; + } + + QuillToolbarBaseButtonOptions baseButtonExtraOptions(BuildContext context) { + return context.requireQuillToolbarBaseButtonOptions; + } + + IconData _iconData(BuildContext context) { + return options.iconData ?? + baseButtonExtraOptions(context).iconData ?? + Icons.movie_creation; + } + + String _tooltip(BuildContext context) { + return options.tooltip ?? + baseButtonExtraOptions(context).tooltip ?? + 'Insert video'; + // ('Insert video'.i18n); + } + + void _sharedOnPressed(BuildContext context) { + _onPressedHandler(context); + _afterButtonPressed(context); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + final iconTheme = _iconTheme(context); + + final tooltip = _tooltip(context); + final iconSize = _iconSize(context); + final iconData = _iconData(context); + final childBuilder = + options.childBuilder ?? baseButtonExtraOptions(context).childBuilder; + + final iconColor = iconTheme?.iconUnselectedColor ?? theme.iconTheme.color; + final iconFillColor = iconTheme?.iconUnselectedFillColor ?? + (options.fillColor ?? theme.canvasColor); + + if (childBuilder != null) { + return childBuilder( + QuillToolbarVideoButtonOptions( + afterButtonPressed: _afterButtonPressed(context), + iconData: iconData, + dialogTheme: options.dialogTheme, + fillColor: iconFillColor, + iconSize: options.iconSize, + iconButtonFactor: options.iconButtonFactor, + linkRegExp: options.linkRegExp, + tooltip: options.tooltip, + iconTheme: options.iconTheme, + videoConfigurations: options.videoConfigurations, + ), + QuillToolbarVideoButtonExtraOptions( + context: context, + controller: controller, + onPressed: () => _sharedOnPressed(context), + ), + ); + } + + return QuillToolbarIconButton( + icon: Icon(iconData, size: iconSize, color: iconColor), + tooltip: tooltip, + highlightElevation: 0, + hoverElevation: 0, + size: iconSize * 1.77, + fillColor: iconFillColor, + borderRadius: iconTheme?.borderRadius ?? 2, + onPressed: () => _sharedOnPressed(context), + ); + } + + Future _onPressedHandler(BuildContext context) async { + final imagePickerService = + QuillSharedExtensionsConfigurations.get(context: context) + .imagePickerService; + + final onRequestPickVideo = options.videoConfigurations.onRequestPickVideo; + if (onRequestPickVideo != null) { + final videoUrl = await onRequestPickVideo(context, imagePickerService); + if (videoUrl != null) { + await options.videoConfigurations + .onVideoInsertCallback(videoUrl, controller); + await options.videoConfigurations.onVideoInsertedCallback + ?.call(videoUrl); + } + return; + } + + final imageSource = await showSelectVideoSourceDialog(context: context); + + if (imageSource == null) { + return; + } + + final videoUrl = switch (imageSource) { + InsertVideoSource.gallery => + (await imagePickerService.pickVideo(source: ImageSource.gallery))?.path, + InsertVideoSource.camera => + (await imagePickerService.pickVideo(source: ImageSource.camera))?.path, + InsertVideoSource.link => await _typeLink(context), + }; + if (videoUrl == null) { + return; + } + + if (videoUrl.trim().isNotEmpty) { + await options.videoConfigurations + .onVideoInsertCallback(videoUrl, controller); + await options.videoConfigurations.onVideoInsertedCallback?.call(videoUrl); + } + + // if (options.onVideoPickCallback != null) { + // final selector = options.mediaPickSettingSelector ?? + // ImageVideoUtils.selectMediaPickSetting; + // final source = await selector(context); + // if (source != null) { + // if (source == MediaPickSetting.gallery) { + // } else { + // await _typeLink(context); + // } + // } + // } else {} + } + + Future _typeLink(BuildContext context) async { + final value = await showDialog( + context: context, + builder: (_) => TypeLinkDialog( + dialogTheme: options.dialogTheme, + linkType: LinkType.video, + ), + ); + return value; + } + + // void _linkSubmitted(String? value) { + // if (value != null && value.isNotEmpty) { + // final index = controller.selection.baseOffset; + // final length = controller.selection.extentOffset - index; + + // controller.replaceText(index, length, BlockEmbed.video(value), null); + // } + // } +} diff --git a/flutter_quill_extensions/lib/embeds/widgets/image.dart b/flutter_quill_extensions/lib/presentation/embeds/widgets/image.dart similarity index 68% rename from flutter_quill_extensions/lib/embeds/widgets/image.dart rename to flutter_quill_extensions/lib/presentation/embeds/widgets/image.dart index fe698a43..13c77eb9 100644 --- a/flutter_quill_extensions/lib/embeds/widgets/image.dart +++ b/flutter_quill_extensions/lib/presentation/embeds/widgets/image.dart @@ -5,8 +5,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_quill/flutter_quill.dart'; import 'package:photo_view/photo_view.dart'; -import '../embed_types.dart'; -import '../utils.dart'; +import '../../models/config/editor/image/image.dart'; +import '../../utils/utils.dart'; +import '../embed_types/image.dart'; const List imageFileExtensions = [ '.jpeg', @@ -28,39 +29,48 @@ String getImageStyleString(QuillController controller) { return s ?? ''; } -Image getQuillImageByUrl( - String imageUrl, { +/// [imageProviderBuilder] To override the return value pass value to it +/// [imageSource] The source of the image in the quill delta json document +/// It could be http, file, network, asset, or base 64 image +ImageProvider getImageProviderByImageSource( + String imageSource, { required ImageEmbedBuilderProviderBuilder? imageProviderBuilder, - required ImageErrorWidgetBuilder? imageErrorWidgetBuilder, - double? width, - double? height, - AlignmentGeometry alignment = Alignment.center, + required String assetsPrefix, }) { - if (isImageBase64(imageUrl)) { - return Image.memory(base64.decode(imageUrl), - width: width, height: height, alignment: alignment); + if (imageProviderBuilder != null) { + return imageProviderBuilder(imageSource); } - if (imageProviderBuilder != null) { - return Image( - image: imageProviderBuilder(imageUrl), - width: width, - height: height, - alignment: alignment, - errorBuilder: imageErrorWidgetBuilder, - ); + if (isImageBase64(imageSource)) { + return MemoryImage(base64.decode(imageSource)); } - if (isHttpBasedUrl(imageUrl)) { - return Image.network( - imageUrl, - width: width, - height: height, - alignment: alignment, - errorBuilder: imageErrorWidgetBuilder, - ); + + if (isHttpBasedUrl(imageSource)) { + return NetworkImage(imageSource); + } + + if (imageSource.startsWith(assetsPrefix)) { + // TODO: This impl could be improved + return AssetImage(imageSource); } - return Image.file( - File(imageUrl), + return FileImage(File(imageSource)); +} + +Image getImageWidgetByImageSource( + String imageSource, { + required ImageEmbedBuilderProviderBuilder? imageProviderBuilder, + required ImageErrorWidgetBuilder? imageErrorWidgetBuilder, + required String assetsPrefix, + double? width, + double? height, + AlignmentGeometry alignment = Alignment.center, +}) { + return Image( + image: getImageProviderByImageSource( + imageSource, + imageProviderBuilder: imageProviderBuilder, + assetsPrefix: assetsPrefix, + ), width: width, height: height, alignment: alignment, @@ -97,27 +107,14 @@ String appendFileExtensionToImageUrl(String url) { class ImageTapWrapper extends StatelessWidget { const ImageTapWrapper({ required this.imageUrl, - required this.imageProviderBuilder, - required this.imageErrorWidgetBuilder, + required this.configurations, + required this.assetsPrefix, + super.key, }); final String imageUrl; - final ImageEmbedBuilderProviderBuilder? imageProviderBuilder; - final ImageEmbedBuilderErrorWidgetBuilder? imageErrorWidgetBuilder; - - ImageProvider _imageProviderByUrl( - String imageUrl, { - required ImageEmbedBuilderProviderBuilder? customImageProviderBuilder, - }) { - if (customImageProviderBuilder != null) { - return customImageProviderBuilder(imageUrl); - } - if (isHttpBasedUrl(imageUrl)) { - return NetworkImage(imageUrl); - } - - return FileImage(File(imageUrl)); - } + final QuillEditorImageEmbedConfigurations configurations; + final String assetsPrefix; @override Widget build(BuildContext context) { @@ -129,11 +126,12 @@ class ImageTapWrapper extends StatelessWidget { child: Stack( children: [ PhotoView( - imageProvider: _imageProviderByUrl( + imageProvider: getImageProviderByImageSource( imageUrl, - customImageProviderBuilder: imageProviderBuilder, + imageProviderBuilder: configurations.imageProviderBuilder, + assetsPrefix: assetsPrefix, ), - errorBuilder: imageErrorWidgetBuilder, + errorBuilder: configurations.imageErrorWidgetBuilder, loadingBuilder: (context, event) { return Container( color: Colors.black, @@ -168,8 +166,11 @@ class ImageTapWrapper extends StatelessWidget { bottom: 0, left: 0, right: 0, - child: - Icon(Icons.close, color: Colors.grey[400], size: 28), + child: Icon( + Icons.close, + color: Colors.grey[400], + size: 28, + ), ) ], ), diff --git a/flutter_quill_extensions/lib/presentation/embeds/widgets/image_resizer.dart b/flutter_quill_extensions/lib/presentation/embeds/widgets/image_resizer.dart new file mode 100644 index 00000000..0e7f90cf --- /dev/null +++ b/flutter_quill_extensions/lib/presentation/embeds/widgets/image_resizer.dart @@ -0,0 +1,139 @@ +import 'package:flutter/cupertino.dart' + show CupertinoActionSheet, CupertinoActionSheetAction; +import 'package:flutter/foundation.dart' show defaultTargetPlatform; +import 'package:flutter/material.dart' show Slider, Card; +import 'package:flutter/scheduler.dart' show SchedulerBinding; +import 'package:flutter/widgets.dart'; +import 'package:flutter_quill/translations.dart'; + +class ImageResizer extends StatefulWidget { + const ImageResizer({ + required this.imageWidth, + required this.imageHeight, + required this.maxWidth, + required this.maxHeight, + required this.onImageResize, + super.key, + }); + + final double? imageWidth; + final double? imageHeight; + final double maxWidth; + final double maxHeight; + final Function(double width, double height) onImageResize; + + @override + ImageResizerState createState() => ImageResizerState(); +} + +class ImageResizerState extends State { + late double _width; + late double _height; + + @override + void initState() { + super.initState(); + _width = widget.imageWidth ?? widget.maxWidth; + _height = widget.imageHeight ?? widget.maxHeight; + } + + @override + Widget build(BuildContext context) { + switch (defaultTargetPlatform) { + case TargetPlatform.iOS: + return _showCupertinoMenu(); + case TargetPlatform.android: + return _showMaterialMenu(); + case TargetPlatform.macOS: + case TargetPlatform.windows: + case TargetPlatform.linux: + case TargetPlatform.fuchsia: + return _showMaterialMenu(); + default: + throw UnsupportedError( + 'Not supposed to be invoked for $defaultTargetPlatform', + ); + } + } + + Widget _showMaterialMenu() { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + _widthSlider(), + _heightSlider(), + ], + ); + } + + Widget _showCupertinoMenu() { + return CupertinoActionSheet( + actions: [ + CupertinoActionSheetAction( + onPressed: () {}, + child: _widthSlider(), + ), + CupertinoActionSheetAction( + onPressed: () {}, + child: _heightSlider(), + ) + ], + ); + } + + Widget _slider({ + required bool isWidth, + required ValueChanged onChanged, + }) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: Card( + child: Slider( + value: isWidth ? _width : _height, + max: isWidth ? widget.maxWidth : widget.maxHeight, + divisions: 1000, + // Might need to be changed + label: isWidth ? context.loc.width : context.loc.height, + onChanged: (val) { + setState(() { + onChanged(val); + _resizeImage(); + }); + }, + ), + ), + ); + } + + Widget _heightSlider() { + return _slider( + isWidth: false, + onChanged: (value) { + _height = value; + }, + ); + } + + Widget _widthSlider() { + return _slider( + isWidth: true, + onChanged: (value) { + _width = value; + }, + ); + } + + bool _scheduled = false; + + void _resizeImage() { + if (_scheduled) { + return; + } + + _scheduled = true; + SchedulerBinding.instance.addPostFrameCallback((_) { + widget.onImageResize(_width, _height); + _scheduled = false; + }); + } +} diff --git a/flutter_quill_extensions/lib/embeds/widgets/video_app.dart b/flutter_quill_extensions/lib/presentation/embeds/widgets/video_app.dart similarity index 93% rename from flutter_quill_extensions/lib/embeds/widgets/video_app.dart rename to flutter_quill_extensions/lib/presentation/embeds/widgets/video_app.dart index c65b9a78..5b794394 100644 --- a/flutter_quill_extensions/lib/embeds/widgets/video_app.dart +++ b/flutter_quill_extensions/lib/presentation/embeds/widgets/video_app.dart @@ -6,6 +6,8 @@ import 'package:flutter_quill/flutter_quill.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:video_player/video_player.dart'; +import '../../../flutter_quill_extensions.dart'; + /// Widget for playing back video /// Refer to https://github.com/flutter/plugins/tree/master/packages/video_player/video_player class VideoApp extends StatefulWidget { @@ -13,6 +15,7 @@ class VideoApp extends StatefulWidget { required this.videoUrl, required this.context, required this.readOnly, + super.key, this.onVideoInit, }); @@ -22,10 +25,10 @@ class VideoApp extends StatefulWidget { final void Function(GlobalKey videoContainerKey)? onVideoInit; @override - _VideoAppState createState() => _VideoAppState(); + VideoAppState createState() => VideoAppState(); } -class _VideoAppState extends State { +class VideoAppState extends State { late VideoPlayerController _controller; GlobalKey videoContainerKey = GlobalKey(); @@ -33,7 +36,7 @@ class _VideoAppState extends State { void initState() { super.initState(); - _controller = widget.videoUrl.startsWith('http') + _controller = isHttpBasedUrl(widget.videoUrl) ? VideoPlayerController.networkUrl(Uri.parse(widget.videoUrl)) : VideoPlayerController.file(File(widget.videoUrl)) ..initialize().then((_) { @@ -81,7 +84,6 @@ class _VideoAppState extends State { return Container( key: videoContainerKey, - // height: 300, child: InkWell( onTap: () { setState(() { diff --git a/flutter_quill_extensions/lib/embeds/widgets/youtube_video_app.dart b/flutter_quill_extensions/lib/presentation/embeds/widgets/youtube_video_app.dart similarity index 54% rename from flutter_quill_extensions/lib/embeds/widgets/youtube_video_app.dart rename to flutter_quill_extensions/lib/presentation/embeds/widgets/youtube_video_app.dart index 02e53fbe..71d6fb5c 100644 --- a/flutter_quill_extensions/lib/embeds/widgets/youtube_video_app.dart +++ b/flutter_quill_extensions/lib/presentation/embeds/widgets/youtube_video_app.dart @@ -1,23 +1,26 @@ -import 'package:flutter/gestures.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_quill/flutter_quill.dart'; -import 'package:url_launcher/url_launcher.dart'; +import 'package:flutter/gestures.dart' show TapGestureRecognizer; +import 'package:flutter/widgets.dart'; +import 'package:flutter_quill/flutter_quill.dart' show DefaultStyles; +import 'package:url_launcher/url_launcher.dart' show launchUrl; import 'package:youtube_player_flutter/youtube_player_flutter.dart'; class YoutubeVideoApp extends StatefulWidget { const YoutubeVideoApp( - {required this.videoUrl, required this.context, required this.readOnly}); + {required this.videoUrl, + required this.context, + required this.readOnly, + super.key}); final String videoUrl; final BuildContext context; final bool readOnly; @override - _YoutubeVideoAppState createState() => _YoutubeVideoAppState(); + YoutubeVideoAppState createState() => YoutubeVideoAppState(); } -class _YoutubeVideoAppState extends State { - var _youtubeController; +class YoutubeVideoAppState extends State { + YoutubePlayerController? _youtubeController; @override void initState() { @@ -36,26 +39,32 @@ class _YoutubeVideoAppState extends State { @override Widget build(BuildContext context) { final defaultStyles = DefaultStyles.getInstance(context); - if (_youtubeController == null) { + final youtubeController = _youtubeController; + + if (youtubeController == null) { if (widget.readOnly) { return RichText( text: TextSpan( - text: widget.videoUrl, - style: defaultStyles.link, - recognizer: TapGestureRecognizer() - ..onTap = () => launchUrl(Uri.parse(widget.videoUrl))), + text: widget.videoUrl, + style: defaultStyles.link, + recognizer: TapGestureRecognizer() + ..onTap = () => launchUrl( + Uri.parse(widget.videoUrl), + ), + ), ); } return RichText( - text: TextSpan(text: widget.videoUrl, style: defaultStyles.link)); + text: TextSpan(text: widget.videoUrl, style: defaultStyles.link), + ); } - return Container( + return SizedBox( height: 300, child: YoutubePlayerBuilder( player: YoutubePlayer( - controller: _youtubeController, + controller: youtubeController, showVideoProgressIndicator: true, ), builder: (context, player) { @@ -73,7 +82,7 @@ class _YoutubeVideoAppState extends State { @override void dispose() { + _youtubeController?.dispose(); super.dispose(); - _youtubeController.dispose(); } } diff --git a/flutter_quill_extensions/lib/presentation/models/config/editor/image/image.dart b/flutter_quill_extensions/lib/presentation/models/config/editor/image/image.dart new file mode 100644 index 00000000..8d6646f0 --- /dev/null +++ b/flutter_quill_extensions/lib/presentation/models/config/editor/image/image.dart @@ -0,0 +1,162 @@ +import 'dart:io' show File; + +import 'package:flutter/foundation.dart' show VoidCallback; +import 'package:flutter_quill/extensions.dart'; +import 'package:meta/meta.dart' show immutable; + +import '../../../../embeds/embed_types/image.dart'; + +/// [QuillEditorImageEmbedConfigurations] for desktop, mobile and +/// other platforms +/// excluding web, it's configurations that is needed for the editor +/// +@immutable +class QuillEditorImageEmbedConfigurations { + const QuillEditorImageEmbedConfigurations({ + ImageEmbedBuilderOnRemovedCallback? onImageRemovedCallback, + this.shouldRemoveImageCallback, + this.imageProviderBuilder, + this.imageErrorWidgetBuilder, + this.onImageClicked, + }) : _onImageRemovedCallback = onImageRemovedCallback; + + /// [onImageRemovedCallback] is called when an image is + /// removed from the editor. + /// By default, [onImageRemovedCallback] deletes the + /// temporary image file if + /// the platform is mobile and if it still exists. You + /// can customize this behavior + /// by passing your own function that handles the removal process. + /// + /// Example of [onImageRemovedCallback] customization: + /// ```dart + /// afterRemoveImageFromEditor: (imageFile) async { + /// // Your custom logic here + /// // or leave it empty to do nothing + /// } + /// ``` + /// + /// Default value if the passed value is null: + /// [QuillEditorImageEmbedConfigurations.defaultOnImageRemovedCallback] + /// + /// so if you want to do nothing make sure to pass a empty callback + /// instead of passing null as value + final ImageEmbedBuilderOnRemovedCallback? _onImageRemovedCallback; + + ImageEmbedBuilderOnRemovedCallback get onImageRemovedCallback { + return _onImageRemovedCallback ?? + QuillEditorImageEmbedConfigurations.defaultOnImageRemovedCallback; + } + + /// [shouldRemoveImageCallback] is a callback + /// function that is invoked when the + /// user attempts to remove an image from the editor. It allows you to control + /// whether the image should be removed based on your custom logic. + /// + /// Example of [shouldRemoveImageCallback] customization: + /// ```dart + /// shouldRemoveImageFromEditor: (imageFile) async { + /// // Show a confirmation dialog before removing the image + /// final isShouldRemove = await showYesCancelDialog( + /// context: context, + /// options: const YesOrCancelDialogOptions( + /// title: 'Deleting an image', + /// message: 'Are you sure you want' ' to delete this + /// image from the editor?', + /// ), + /// ); + /// + /// // Return `true` to allow image removal if the user confirms, otherwise + /// `false` + /// return isShouldRemove; + /// } + /// ``` + /// + final ImageEmbedBuilderWillRemoveCallback? shouldRemoveImageCallback; + + /// [imageProviderBuilder] if you want to use custom image provider, please + /// pass a value to this property + /// By default we will use [NetworkImage] provider if the image url/path + /// is using http/https, if not then we will use [FileImage] provider + /// If you ovveride this make sure to handle the case where if the [imageUrl] + /// is in the local storage or it does exists in the system file + /// or use the same way we did it + /// + /// Example of [imageProviderBuilder] customization: + /// ```dart + /// imageProviderBuilder: (imageUrl) async { + /// // Example of using cached_network_image package + /// // Don't forgot to check if that image is local or network one + /// return CachedNetworkImageProvider(imageUrl); + /// } + /// ``` + /// + final ImageEmbedBuilderProviderBuilder? imageProviderBuilder; + + /// [imageErrorWidgetBuilder] if you want to show a custom widget based on the + /// exception that happen while loading the image, if it network image or + /// local one, and it will get called on all the images even in the photo + /// preview widget and not just in the quill editor + /// by default the default error from flutter framework will thrown + /// + final ImageEmbedBuilderErrorWidgetBuilder? imageErrorWidgetBuilder; + + /// What should happen when the image is pressed? + /// + /// By default will show `ImageOptionsMenu` dialog + final VoidCallback? onImageClicked; + + static ImageEmbedBuilderOnRemovedCallback get defaultOnImageRemovedCallback { + return (imageUrl) async { + if (isWeb()) { + return; + } + + final mobile = isMobile(supportWeb: false); + // If the platform is not mobile, return void; + // Since the mobile OS gives us a copy of the image + + // Note: We should remove the image on Flutter web + // since the behavior is similar to how it is on mobile, + // but since this builder is not for web, we will ignore it + if (!mobile) { + return; + } + + // On mobile OS (Android, iOS), the system will not give us + // direct access to the image; instead, + // it will give us the image + // in the temp directory of the application. So, we want to + // remove it when we no longer need it. + + // but on desktop we don't want to touch user files + // especially on macOS, where we can't even delete + // it without + // permission + + final dartIoImageFile = File(imageUrl); + + final isFileExists = await dartIoImageFile.exists(); + if (isFileExists) { + await dartIoImageFile.delete(); + } + }; + } + + QuillEditorImageEmbedConfigurations copyWith({ + ImageEmbedBuilderOnRemovedCallback? onImageRemovedCallback, + ImageEmbedBuilderWillRemoveCallback? shouldRemoveImageCallback, + ImageEmbedBuilderProviderBuilder? imageProviderBuilder, + ImageEmbedBuilderErrorWidgetBuilder? imageErrorWidgetBuilder, + bool? forceUseMobileOptionMenuForImageClick, + }) { + return QuillEditorImageEmbedConfigurations( + onImageRemovedCallback: onImageRemovedCallback ?? _onImageRemovedCallback, + shouldRemoveImageCallback: + shouldRemoveImageCallback ?? this.shouldRemoveImageCallback, + imageProviderBuilder: imageProviderBuilder ?? this.imageProviderBuilder, + imageErrorWidgetBuilder: + imageErrorWidgetBuilder ?? this.imageErrorWidgetBuilder, + ); + } +} diff --git a/flutter_quill_extensions/lib/presentation/models/config/editor/image/image_web.dart b/flutter_quill_extensions/lib/presentation/models/config/editor/image/image_web.dart new file mode 100644 index 00000000..8facc8a6 --- /dev/null +++ b/flutter_quill_extensions/lib/presentation/models/config/editor/image/image_web.dart @@ -0,0 +1,11 @@ +import 'package:flutter/widgets.dart' show BoxConstraints; +import 'package:meta/meta.dart' show immutable; + +@immutable +class QuillEditorWebImageEmbedConfigurations { + const QuillEditorWebImageEmbedConfigurations({ + this.constraints, + }); + + final BoxConstraints? constraints; +} diff --git a/flutter_quill_extensions/lib/presentation/models/config/editor/video/video.dart b/flutter_quill_extensions/lib/presentation/models/config/editor/video/video.dart new file mode 100644 index 00000000..53039dee --- /dev/null +++ b/flutter_quill_extensions/lib/presentation/models/config/editor/video/video.dart @@ -0,0 +1,24 @@ +import 'package:flutter/widgets.dart' show GlobalKey; +import 'package:meta/meta.dart' show immutable; + +@immutable +class QuillEditorVideoEmbedConfigurations { + const QuillEditorVideoEmbedConfigurations({ + this.onVideoInit, + }); + + /// [onVideoInit] is a callback function that gets triggered when + /// a video is initialized. + /// You can use this to perform actions or setup configurations related + /// to video embedding. + /// + /// + /// Example usage: + /// ```dart + /// onVideoInit: (videoContainerKey) { + /// // Custom video initialization logic + /// }, + /// // Customize other callback functions as needed + /// ``` + final void Function(GlobalKey videoContainerKey)? onVideoInit; +} diff --git a/flutter_quill_extensions/lib/presentation/models/config/editor/video/video_web.dart b/flutter_quill_extensions/lib/presentation/models/config/editor/video/video_web.dart new file mode 100644 index 00000000..26d7f110 --- /dev/null +++ b/flutter_quill_extensions/lib/presentation/models/config/editor/video/video_web.dart @@ -0,0 +1,11 @@ +import 'package:flutter/widgets.dart' show BoxConstraints; +import 'package:meta/meta.dart' show immutable; + +@immutable +class QuillEditorWebVideoEmbedConfigurations { + const QuillEditorWebVideoEmbedConfigurations({ + this.constraints, + }); + + final BoxConstraints? constraints; +} diff --git a/flutter_quill_extensions/lib/presentation/models/config/editor/webview.dart b/flutter_quill_extensions/lib/presentation/models/config/editor/webview.dart new file mode 100644 index 00000000..0d2149c6 --- /dev/null +++ b/flutter_quill_extensions/lib/presentation/models/config/editor/webview.dart @@ -0,0 +1,6 @@ +import 'package:meta/meta.dart' show immutable; + +@immutable +class QuillEditorWebViewEmbedConfigurations { + const QuillEditorWebViewEmbedConfigurations(); +} diff --git a/flutter_quill_extensions/lib/presentation/models/config/toolbar/buttons/camera.dart b/flutter_quill_extensions/lib/presentation/models/config/toolbar/buttons/camera.dart new file mode 100644 index 00000000..dafdf1ab --- /dev/null +++ b/flutter_quill_extensions/lib/presentation/models/config/toolbar/buttons/camera.dart @@ -0,0 +1,36 @@ +import 'package:flutter/widgets.dart' show Color; +import 'package:flutter_quill/flutter_quill.dart'; + +import '../../../../embeds/embed_types/camera.dart'; + +class QuillToolbarCameraButtonExtraOptions + extends QuillToolbarBaseButtonExtraOptions { + const QuillToolbarCameraButtonExtraOptions({ + required super.controller, + required super.context, + required super.onPressed, + }); +} + +class QuillToolbarCameraButtonOptions extends QuillToolbarBaseButtonOptions< + QuillToolbarCameraButtonOptions, QuillToolbarCameraButtonExtraOptions> { + const QuillToolbarCameraButtonOptions({ + this.cameraConfigurations = const QuillToolbarCameraConfigurations(), + this.iconSize, + this.iconButtonFactor, + this.fillColor, + super.iconData, + super.afterButtonPressed, + super.tooltip, + super.iconTheme, + super.childBuilder, + super.controller, + }); + + final double? iconSize; + final double? iconButtonFactor; + + final Color? fillColor; + + final QuillToolbarCameraConfigurations cameraConfigurations; +} diff --git a/flutter_quill_extensions/lib/presentation/models/config/toolbar/buttons/formula.dart b/flutter_quill_extensions/lib/presentation/models/config/toolbar/buttons/formula.dart new file mode 100644 index 00000000..638e794c --- /dev/null +++ b/flutter_quill_extensions/lib/presentation/models/config/toolbar/buttons/formula.dart @@ -0,0 +1,30 @@ +import 'package:flutter/widgets.dart' show Color; +import 'package:flutter_quill/flutter_quill.dart'; + +class QuillToolbarFormulaButtonExtraOptions + extends QuillToolbarBaseButtonExtraOptions { + const QuillToolbarFormulaButtonExtraOptions({ + required super.controller, + required super.context, + required super.onPressed, + }); +} + +class QuillToolbarFormulaButtonOptions extends QuillToolbarBaseButtonOptions< + QuillToolbarFormulaButtonOptions, QuillToolbarFormulaButtonExtraOptions> { + const QuillToolbarFormulaButtonOptions({ + super.tooltip, + super.iconData, + super.iconTheme, + super.afterButtonPressed, + super.childBuilder, + this.fillColor, + this.iconSize, + this.iconButtonFactor, + }); + + final Color? fillColor; + + final double? iconSize; + final double? iconButtonFactor; +} diff --git a/flutter_quill_extensions/lib/presentation/models/config/toolbar/buttons/image.dart b/flutter_quill_extensions/lib/presentation/models/config/toolbar/buttons/image.dart new file mode 100644 index 00000000..d1e5efbd --- /dev/null +++ b/flutter_quill_extensions/lib/presentation/models/config/toolbar/buttons/image.dart @@ -0,0 +1,46 @@ +import 'package:flutter/widgets.dart' show Color; +import 'package:flutter_quill/flutter_quill.dart'; +import 'package:meta/meta.dart' show immutable; + +import '../../../../embeds/embed_types/image.dart'; + +class QuillToolbarImageButtonExtraOptions + extends QuillToolbarBaseButtonExtraOptions { + const QuillToolbarImageButtonExtraOptions({ + required super.controller, + required super.context, + required super.onPressed, + }); +} + +@immutable +class QuillToolbarImageButtonOptions extends QuillToolbarBaseButtonOptions< + QuillToolbarImageButtonOptions, QuillToolbarImageButtonExtraOptions> { + const QuillToolbarImageButtonOptions({ + super.iconData, + super.controller, + this.iconSize, + this.iconButtonFactor, + + /// specifies the tooltip text for the image button. + super.tooltip, + super.afterButtonPressed, + super.childBuilder, + super.iconTheme, + this.fillColor, + this.dialogTheme, + this.linkRegExp, + this.imageButtonConfigurations = const QuillToolbarImageConfigurations(), + }); + + final double? iconSize; + final double? iconButtonFactor; + final Color? fillColor; + + final QuillDialogTheme? dialogTheme; + + /// [imageLinkRegExp] is a regular expression to identify image links. + final RegExp? linkRegExp; + + final QuillToolbarImageConfigurations imageButtonConfigurations; +} diff --git a/flutter_quill_extensions/lib/presentation/models/config/toolbar/buttons/media_button.dart b/flutter_quill_extensions/lib/presentation/models/config/toolbar/buttons/media_button.dart new file mode 100644 index 00000000..d55f20f1 --- /dev/null +++ b/flutter_quill_extensions/lib/presentation/models/config/toolbar/buttons/media_button.dart @@ -0,0 +1,75 @@ +import 'package:flutter/widgets.dart' show AutovalidateMode; +import 'package:flutter/widgets.dart' show Color, Size; +import 'package:flutter_quill/flutter_quill.dart'; + +import '../../../../embeds/embed_types.dart'; + +class QuillToolbarMediaButtonExtraOptions + extends QuillToolbarBaseButtonExtraOptions { + const QuillToolbarMediaButtonExtraOptions({ + required super.controller, + required super.context, + required super.onPressed, + }); +} + +class QuillToolbarMediaButtonOptions extends QuillToolbarBaseButtonOptions< + QuillToolbarMediaButtonOptions, QuillToolbarMediaButtonExtraOptions> { + const QuillToolbarMediaButtonOptions({ + required this.type, + required this.onMediaPickedCallback, + // required this.onVideoPickCallback, + this.dialogBarrierColor, + this.mediaFilePicker, + this.childrenSpacing = 16.0, + this.autovalidateMode = AutovalidateMode.disabled, + this.iconSize, + this.fillColor, + this.dialogTheme, + this.labelText, + this.hintText, + this.submitButtonText, + this.submitButtonSize, + this.galleryButtonText, + this.linkButtonText, + this.validationMessage, + super.iconData, + super.afterButtonPressed, + super.tooltip, + super.iconTheme, + super.childBuilder, + super.controller, + }); + + final double? iconSize; + final Color? fillColor; + final QuillMediaType type; + final QuillDialogTheme? dialogTheme; + final MediaFilePicker? mediaFilePicker; + final MediaPickedCallback? onMediaPickedCallback; + final Color? dialogBarrierColor; + + /// The margin between child widgets in the dialog. + final double childrenSpacing; + + /// The text of label in link add mode. + final String? labelText; + + /// The hint text for link [TextField]. + final String? hintText; + + /// The text of the submit button. + final String? submitButtonText; + + /// The size of dialog buttons. + final Size? submitButtonSize; + + /// The text of the gallery button [MediaSourceSelectorDialog]. + final String? galleryButtonText; + + /// The text of the link button [MediaSourceSelectorDialog]. + final String? linkButtonText; + + final AutovalidateMode autovalidateMode; + final String? validationMessage; +} diff --git a/flutter_quill_extensions/lib/presentation/models/config/toolbar/buttons/video.dart b/flutter_quill_extensions/lib/presentation/models/config/toolbar/buttons/video.dart new file mode 100644 index 00000000..e7efba51 --- /dev/null +++ b/flutter_quill_extensions/lib/presentation/models/config/toolbar/buttons/video.dart @@ -0,0 +1,40 @@ +import 'package:flutter/widgets.dart' show Color; +import 'package:flutter_quill/flutter_quill.dart'; + +import '../../../../embeds/embed_types/video.dart'; + +class QuillToolbarVideoButtonExtraOptions + extends QuillToolbarBaseButtonExtraOptions { + const QuillToolbarVideoButtonExtraOptions({ + required super.controller, + required super.context, + required super.onPressed, + }); +} + +class QuillToolbarVideoButtonOptions extends QuillToolbarBaseButtonOptions< + QuillToolbarVideoButtonOptions, QuillToolbarVideoButtonExtraOptions> { + const QuillToolbarVideoButtonOptions({ + this.linkRegExp, + this.dialogTheme, + this.fillColor, + this.iconSize, + this.iconButtonFactor, + super.iconData, + super.afterButtonPressed, + super.tooltip, + super.iconTheme, + super.childBuilder, + super.controller, + this.videoConfigurations = const QuillToolbarVideoConfigurations(), + }); + + final RegExp? linkRegExp; + final QuillDialogTheme? dialogTheme; + final QuillToolbarVideoConfigurations videoConfigurations; + + final Color? fillColor; + + final double? iconSize; + final double? iconButtonFactor; +} diff --git a/flutter_quill_extensions/lib/presentation/utils/utils.dart b/flutter_quill_extensions/lib/presentation/utils/utils.dart new file mode 100644 index 00000000..00e986fd --- /dev/null +++ b/flutter_quill_extensions/lib/presentation/utils/utils.dart @@ -0,0 +1,201 @@ +import 'dart:io' show File; + +import 'package:flutter/foundation.dart' show immutable; +import 'package:flutter/widgets.dart' show Alignment; +import 'package:flutter_quill/extensions.dart' as base; +import 'package:flutter_quill/flutter_quill.dart' show Attribute, Node; +import '../../logic/extensions/attribute.dart'; +import '../../logic/services/image_saver/s_image_saver.dart'; +import '../embeds/widgets/image.dart'; + +RegExp _base64 = RegExp( + r'^(?:[A-Za-z0-9+\/]{4})*(?:[A-Za-z0-9+\/]{2}==|[A-Za-z0-9+\/]{3}=|[A-Za-z0-9+\/]{4})$', +); + +bool isBase64(String str) { + return _base64.hasMatch(str); +} + +bool isHttpBasedUrl(String url) { + try { + final uri = Uri.parse(url.trim()); + return uri.isScheme('HTTP') || uri.isScheme('HTTPS'); + } catch (_) { + return false; + } +} + +bool isImageBase64(String imageUrl) { + return !isHttpBasedUrl(imageUrl) && isBase64(imageUrl); +} + +bool isYouTubeUrl(String videoUrl) { + try { + final uri = Uri.parse(videoUrl); + return uri.host == 'www.youtube.com' || + uri.host == 'youtube.com' || + uri.host == 'youtu.be' || + uri.host == 'www.youtu.be'; + } catch (_) { + return false; + } +} + +enum SaveImageResultMethod { network, localStorage } + +@immutable +class SaveImageResult { + const SaveImageResult({required this.error, required this.method}); + + final String? error; + final SaveImageResultMethod method; +} + +Future saveImage({ + required String imageUrl, + required ImageSaverService imageSaverService, +}) async { + final imageFile = File(imageUrl); + final hasPermission = await imageSaverService.hasAccess(); + if (!hasPermission) { + await imageSaverService.requestAccess(); + } + final imageExistsLocally = await imageFile.exists(); + if (!imageExistsLocally) { + try { + await imageSaverService.saveImageFromNetwork( + Uri.parse(appendFileExtensionToImageUrl(imageUrl)), + ); + return const SaveImageResult( + error: null, + method: SaveImageResultMethod.network, + ); + } catch (e) { + return SaveImageResult( + error: e.toString(), + method: SaveImageResultMethod.network, + ); + } + } + try { + await imageSaverService.saveLocalImage(imageUrl); + return const SaveImageResult( + error: null, + method: SaveImageResultMethod.localStorage, + ); + } catch (e) { + return SaveImageResult( + error: e.toString(), + method: SaveImageResultMethod.localStorage, + ); + } +} + +( + OptionalSize elementSize, + double? margin, + Alignment alignment, +) getElementAttributes( + Node node, +) { + var elementSize = const OptionalSize(null, null); + var elementAlignment = Alignment.center; + double? elementMargin; + + // Usually double value + final heightValue = double.tryParse( + node.style.attributes[Attribute.height.key]?.value.toString() ?? ''); + final widthValue = double.tryParse( + node.style.attributes[Attribute.width.key]?.value.toString() ?? ''); + + if (heightValue != null) { + elementSize = elementSize.copyWith( + height: heightValue, + ); + } + if (widthValue != null) { + elementSize = elementSize.copyWith( + width: widthValue, + ); + } + + final cssStyle = node.style.attributes['style']; + + if (cssStyle != null) { + final attrs = base.isMobile(supportWeb: false) + ? base.parseKeyValuePairs(cssStyle.value.toString(), { + AttributeExt.mobileWidth.key, + AttributeExt.mobileHeight.key, + AttributeExt.mobileMargin.key, + AttributeExt.mobileAlignment.key, + }) + : base.parseKeyValuePairs(cssStyle.value.toString(), { + Attribute.width.key, + Attribute.height.key, + 'margin', + 'alignment', + }); + if (attrs.isEmpty) { + return (elementSize, elementMargin, elementAlignment); + } + + // It css value as string but we will try to support it anyway + + // TODO: This could be improved much better + final cssHeightValue = double.tryParse(((base.isMobile(supportWeb: false) + ? attrs[AttributeExt.mobileHeight.key] + : attrs[Attribute.height.key]) ?? + '') + .replaceFirst('px', '')); + final cssWidthValue = double.tryParse(((!base.isMobile(supportWeb: false) + ? attrs[Attribute.width.key] + : attrs[AttributeExt.mobileWidth.key]) ?? + '') + .replaceFirst('px', '')); + + if (cssHeightValue != null) { + elementSize = elementSize.copyWith(height: cssHeightValue); + } + if (cssWidthValue != null) { + elementSize = elementSize.copyWith(width: cssWidthValue); + } + + elementAlignment = base.getAlignment(base.isMobile(supportWeb: false) + ? attrs[AttributeExt.mobileAlignment.key] + : attrs['alignment']); + final margin = (base.isMobile(supportWeb: false) + ? double.tryParse(AttributeExt.mobileMargin.key) + : double.tryParse('margin')); + if (margin != null) { + elementMargin = margin; + } + } + + return (elementSize, elementMargin, elementAlignment); +} + +@immutable +class OptionalSize { + const OptionalSize( + this.width, + this.height, + ); + + /// If non-null, requires the child to have exactly this width. + /// If null, the child is free to choose its own width. + final double? width; + + /// If non-null, requires the child to have exactly this height. + /// If null, the child is free to choose its own height. + final double? height; + + OptionalSize copyWith({ + double? width, + double? height, + }) { + return OptionalSize( + width ?? this.width, + height ?? this.height, + ); + } +} diff --git a/flutter_quill_extensions/lib/presentation/utils/web_utils.dart b/flutter_quill_extensions/lib/presentation/utils/web_utils.dart new file mode 100644 index 00000000..b34d308f --- /dev/null +++ b/flutter_quill_extensions/lib/presentation/utils/web_utils.dart @@ -0,0 +1,60 @@ +import 'package:flutter_quill/extensions.dart' as base; +import 'package:flutter_quill/flutter_quill.dart' show Attribute, Node; + +/// Prefer the width, and height from the css style attribute if exits +/// it can be `auto` or `100px` so it's specific to HTML && CSS +/// if not, we will use the one from attributes which is usually just an double +( + String height, + String width, + String margin, + String alignment, +) getWebElementAttributes( + Node node, +) { + var height = 'auto'; + var width = 'auto'; + // TODO: Add support for margin and alignment + const margin = 'auto'; + const alignment = 'center'; + + // return (height, width, margin, alignment); + + final cssStyle = node.style.attributes['style']; + + // Usually double value + final heightValue = node.style.attributes[Attribute.height.key]?.value; + final widthValue = node.style.attributes[Attribute.width.key]?.value; + + if (cssStyle != null) { + final attrs = base.parseKeyValuePairs(cssStyle.value.toString(), { + Attribute.width.key, + Attribute.height.key, + 'margin', + 'alignment', + }); + final cssHeightValue = attrs[Attribute.height.key]; + if (cssHeightValue != null) { + height = cssHeightValue; + } else { + height = '${heightValue}px'; + } + final cssWidthValue = attrs[Attribute.width.key]; + if (cssWidthValue != null) { + width = cssWidthValue; + } else if (widthValue != null) { + width = '${widthValue}px'; + } + + return (height, width, margin, alignment); + } + + if (heightValue != null) { + height = '${heightValue}px'; + } + if (widthValue != null) { + width = '${widthValue}px'; + } + + return (height, width, margin, alignment); +} diff --git a/flutter_quill_extensions/lib/shims/dart_ui_fake.dart b/flutter_quill_extensions/lib/shims/dart_ui_fake.dart deleted file mode 100644 index baaf9ebd..00000000 --- a/flutter_quill_extensions/lib/shims/dart_ui_fake.dart +++ /dev/null @@ -1,23 +0,0 @@ -// ignore_for_file: avoid_classes_with_only_static_members, camel_case_types, lines_longer_than_80_chars - -import 'package:universal_html/html.dart' as html; - -// Fake interface for the logic that this package needs from (web-only) dart:ui. -// This is conditionally exported so the analyzer sees these methods as available. - -typedef PlatroformViewFactory = html.Element Function(int viewId); - -/// Shim for web_ui engine.PlatformViewRegistry -/// https://github.com/flutter/engine/blob/master/lib/web_ui/lib/ui.dart#L62 -class platformViewRegistry { - /// Shim for registerViewFactory - /// https://github.com/flutter/engine/blob/master/lib/web_ui/lib/ui.dart#L72 - static dynamic registerViewFactory( - String viewTypeId, PlatroformViewFactory viewFactory) {} -} - -/// Shim for web_ui engine.AssetManager -/// https://github.com/flutter/engine/blob/master/lib/web_ui/lib/src/engine/assets.dart#L12 -class webOnlyAssetManager { - static dynamic getAssetUrl(String asset) {} -} diff --git a/flutter_quill_extensions/pubspec.yaml b/flutter_quill_extensions/pubspec.yaml index 7e2259e5..38065479 100644 --- a/flutter_quill_extensions/pubspec.yaml +++ b/flutter_quill_extensions/pubspec.yaml @@ -1,8 +1,15 @@ name: flutter_quill_extensions description: Embed extensions for flutter_quill including image, video, formula and etc. -version: 0.5.1 -homepage: https://bulletjournal.us/home/index.html +version: 0.6.10 +homepage: https://github.com/singerdmx/flutter-quill/tree/master/flutter_quill_extensions repository: https://github.com/singerdmx/flutter-quill/tree/master/flutter_quill_extensions + +topics: + - ui + - widgets + - widget + - rich-text-editor + platforms: android: ios: @@ -12,31 +19,35 @@ platforms: windows: environment: - sdk: ">=2.17.0 <4.0.0" - flutter: ">=3.0.0" + sdk: '>=3.1.3 <4.0.0' + flutter: ">=1.17.0" dependencies: flutter: sdk: flutter - flutter_quill: ^7.8.0 - # In case you are working on changes for both libraries, - # flutter_quill: - # path: ../ - + # Normal packages http: ^1.1.0 - image_picker: ">=1.0.4" - photo_view: ^0.14.0 - video_player: ^2.7.2 - youtube_player_flutter: ^8.1.2 - math_keyboard: ">=0.2.1" + path: ^1.8.3 + meta: ^1.9.1 universal_html: ^2.2.4 + cross_file: ^0.3.3+6 + + flutter_quill: ^8.5.0 + photo_view: ^0.14.0 - gal: ^2.1.2 + # Plugins + video_player: ^2.8.1 + youtube_player_flutter: ^8.1.2 + flutter_inappwebview: ^5.8.0 + gal: ^2.1.3 + image_picker: ^1.0.4 + url_launcher: ^6.2.1 dev_dependencies: flutter_test: sdk: flutter - pedantic: ^1.11.1 + flutter_lints: ^3.0.1 flutter: + uses-material-design: true \ No newline at end of file diff --git a/flutter_quill_extensions/pubspec_overrides.yaml.disabled b/flutter_quill_extensions/pubspec_overrides.yaml.disabled new file mode 100644 index 00000000..5593142e --- /dev/null +++ b/flutter_quill_extensions/pubspec_overrides.yaml.disabled @@ -0,0 +1,3 @@ +dependency_overrides: + flutter_quill: + path: ../ \ No newline at end of file diff --git a/flutter_quill_test/.gitignore b/flutter_quill_test/.gitignore new file mode 100644 index 00000000..96486fd9 --- /dev/null +++ b/flutter_quill_test/.gitignore @@ -0,0 +1,30 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +/pubspec.lock +**/doc/api/ +.dart_tool/ +.packages +build/ diff --git a/flutter_quill_test/.metadata b/flutter_quill_test/.metadata new file mode 100644 index 00000000..6176c000 --- /dev/null +++ b/flutter_quill_test/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "d211f42860350d914a5ad8102f9ec32764dc6d06" + channel: "stable" + +project_type: package diff --git a/flutter_quill_test/.pubignore b/flutter_quill_test/.pubignore new file mode 100644 index 00000000..757d78e3 --- /dev/null +++ b/flutter_quill_test/.pubignore @@ -0,0 +1,3 @@ +# For local development +pubspec_overrides.yaml +pubspec_overrides.yaml.disabled \ No newline at end of file diff --git a/flutter_quill_test/.test_config b/flutter_quill_test/.test_config new file mode 100644 index 00000000..412fc5c5 --- /dev/null +++ b/flutter_quill_test/.test_config @@ -0,0 +1,3 @@ +{ + "test_package": true +} \ No newline at end of file diff --git a/flutter_quill_test/CHANGELOG.md b/flutter_quill_test/CHANGELOG.md new file mode 100644 index 00000000..324c4355 --- /dev/null +++ b/flutter_quill_test/CHANGELOG.md @@ -0,0 +1,16 @@ +## 0.0.5 +* Update `README.md` + +## 0.0.4 +* Update `README.md` +* Documentation comments. + +## 0.0.3 +* Update the `README.md` and description + +## 0.0.2 +* Add `.test_config` to mark the package as testing package + +## 0.0.1 + +* initial release. diff --git a/flutter_quill_test/LICENSE b/flutter_quill_test/LICENSE new file mode 100644 index 00000000..e82b91ed --- /dev/null +++ b/flutter_quill_test/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Flutter Quill Team + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/flutter_quill_test/README.md b/flutter_quill_test/README.md new file mode 100644 index 00000000..576c2531 --- /dev/null +++ b/flutter_quill_test/README.md @@ -0,0 +1,49 @@ +# Flutter Quill Test + +Test utilities for [flutter_quill](https://pub.dev/packages/flutter_quill) +which include methods to simplify interacting with the editor in test cases. + +## Table of Contents +- [Flutter Quill Test](#flutter-quill-test) + - [Table of Contents](#table-of-contents) + - [Installation](#installation) + - [Testing](#testing) + - [Contributing](#contributing) + +## Installation + +Run the command in your project root folder: +``` +dart pub add dev:flutter_quill_test +``` + +Example of how it will look like: + +```yaml +dev_dependencies: + flutter_quill_test: any # Use latest Version + flutter_lints: any + flutter_test: + sdk: flutter +``` + +## Testing +To aid in testing applications using the editor an extension to the flutter `WidgetTester` is provided which includes methods to simplify interacting with the editor in test cases. + +Import the test utilities in your test file: + +```dart +import 'package:flutter_quill/flutter_quill_test.dart'; +``` + +and then enter text using `quillEnterText`: + +```dart +await tester.quillEnterText(find.byType(QuillEditor), 'test\n'); +``` + +## Contributing + +We welcome contributions! + +Please follow these guidelines when contributing to our project. See [CONTRIBUTING.md](../CONTRIBUTING.md) for more details. \ No newline at end of file diff --git a/flutter_quill_test/analysis_options.yaml b/flutter_quill_test/analysis_options.yaml new file mode 100644 index 00000000..f1a38172 --- /dev/null +++ b/flutter_quill_test/analysis_options.yaml @@ -0,0 +1,36 @@ +include: package:flutter_lints/flutter.yaml + +analyzer: + errors: + undefined_prefixed_name: ignore + unsafe_html: 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: true + avoid_redundant_argument_values: true + 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 diff --git a/lib/flutter_quill_test.dart b/flutter_quill_test/lib/flutter_quill_test.dart similarity index 100% rename from lib/flutter_quill_test.dart rename to flutter_quill_test/lib/flutter_quill_test.dart diff --git a/lib/src/test/widget_tester_extension.dart b/flutter_quill_test/lib/src/test/widget_tester_extension.dart similarity index 64% rename from lib/src/test/widget_tester_extension.dart rename to flutter_quill_test/lib/src/test/widget_tester_extension.dart index 85cc4500..11e5f45b 100644 --- a/lib/src/test/widget_tester_extension.dart +++ b/flutter_quill_test/lib/src/test/widget_tester_extension.dart @@ -1,24 +1,30 @@ import 'package:flutter/material.dart'; +import 'package:flutter_quill/flutter_quill.dart'; import 'package:flutter_test/flutter_test.dart'; -import '../widgets/editor/editor.dart'; -import '../widgets/raw_editor/raw_editor.dart'; - -/// Extends -extension QuillEnterText on WidgetTester { +/// Extensions on [WidgetTester] that have utilities that help +/// simplify interacting with the editor in test cases. +extension QuillWidgetTesterExt on WidgetTester { /// Give the QuillEditor widget specified by [finder] the focus. + /// Future quillGiveFocus(Finder finder) { return TestAsyncUtils.guard(() async { final editor = state( find.descendant( - of: finder, - matching: - find.byType(QuillEditor, skipOffstage: finder.skipOffstage), - matchRoot: true), + of: finder, + matching: find.byType( + QuillEditor, + skipOffstage: finder.skipOffstage, + ), + matchRoot: true, + ), ); editor.widget.focusNode.requestFocus(); await pump(); - expect(editor.widget.focusNode.hasFocus, isTrue); + expect( + editor.widget.focusNode.hasFocus, + isTrue, + ); }); } @@ -28,6 +34,7 @@ extension QuillEnterText on WidgetTester { /// /// The widget specified by [finder] must be a [QuillEditor] or have a /// [QuillEditor] descendant. For example `find.byType(QuillEditor)`. + /// Future quillEnterText(Finder finder, String text) async { return TestAsyncUtils.guard(() async { await quillGiveFocus(finder); @@ -42,18 +49,24 @@ extension QuillEnterText on WidgetTester { /// The widget specified by [finder] must already have focus and be a /// [QuillEditor] or have a [QuillEditor] descendant. For example /// `find.byType(QuillEditor)`. + /// Future quillUpdateEditingValue(Finder finder, String text) async { return TestAsyncUtils.guard(() async { - final editor = state( + final editor = state( find.descendant( - of: finder, - matching: find.byType(RawEditor, skipOffstage: finder.skipOffstage), - matchRoot: true), + of: finder, + matching: + find.byType(QuillRawEditor, skipOffstage: finder.skipOffstage), + matchRoot: true, + ), ); - testTextInput.updateEditingValue(TextEditingValue( + testTextInput.updateEditingValue( + TextEditingValue( text: text, selection: TextSelection.collapsed( - offset: editor.textEditingValue.text.length))); + offset: editor.textEditingValue.text.length), + ), + ); await idle(); }); } diff --git a/flutter_quill_test/pubspec.yaml b/flutter_quill_test/pubspec.yaml new file mode 100644 index 00000000..daee56e5 --- /dev/null +++ b/flutter_quill_test/pubspec.yaml @@ -0,0 +1,37 @@ +name: flutter_quill_test +description: Test utilities for flutter_quill which includes methods to simplify interacting with the editor in test cases. +version: 0.0.5 +homepage: https://1o24bbs.com/c/bulletjournal/108 +repository: https://github.com/singerdmx/flutter-quill/tree/master/flutter_quill_test + +topics: + - ui + - widgets + - widget + - rich-text-editor + - quill + +platforms: + android: + ios: + linux: + macos: + web: + windows: + +environment: + sdk: '>=3.1.5 <4.0.0' + flutter: ">=1.17.0" + +dependencies: + flutter: + sdk: flutter + flutter_quill: ^8.2.5 + flutter_test: + sdk: flutter + +dev_dependencies: + flutter_lints: ^3.0.1 + +flutter: + uses-material-design: true diff --git a/flutter_quill_test/pubspec_overrides.yaml.disabled b/flutter_quill_test/pubspec_overrides.yaml.disabled new file mode 100644 index 00000000..5593142e --- /dev/null +++ b/flutter_quill_test/pubspec_overrides.yaml.disabled @@ -0,0 +1,3 @@ +dependency_overrides: + flutter_quill: + path: ../ \ No newline at end of file diff --git a/flutter_quill_test/test/flutter_quill_test_test.dart b/flutter_quill_test/test/flutter_quill_test_test.dart new file mode 100644 index 00000000..953946ba --- /dev/null +++ b/flutter_quill_test/test/flutter_quill_test_test.dart @@ -0,0 +1,2 @@ +/// This will be empty for now +void main() {} diff --git a/l10n.yaml b/l10n.yaml new file mode 100644 index 00000000..15fabf56 --- /dev/null +++ b/l10n.yaml @@ -0,0 +1,10 @@ +arb-dir: lib/src/l10n +template-arb-file: quill_en.arb +output-localization-file: quill_localizations.dart +output-class: FlutterQuillLocalizations +output-dir: lib/src/l10n/generated +synthetic-package: false +format: true +untranslated-messages-file: lib/src/l10n/untranslated.json +nullable-getter: true +suppress-warnings: false \ No newline at end of file diff --git a/lib/flutter_quill.dart b/lib/flutter_quill.dart index 25f8c901..28fb0696 100644 --- a/lib/flutter_quill.dart +++ b/lib/flutter_quill.dart @@ -1,6 +1,8 @@ library flutter_quill; +export 'src/extensions/quill_provider.dart'; export 'src/models/config/quill_configurations.dart'; +export 'src/models/config/raw_editor/configurations.dart'; export 'src/models/config/toolbar/base_configurations.dart'; export 'src/models/documents/attribute.dart'; export 'src/models/documents/document.dart'; @@ -17,16 +19,17 @@ export 'src/models/structs/link_dialog_action.dart'; export 'src/models/structs/offset_value.dart'; export 'src/models/structs/optional_size.dart'; export 'src/models/structs/vertical_spacing.dart'; -export 'src/models/themes/quill_custom_button.dart'; export 'src/models/themes/quill_dialog_theme.dart'; export 'src/models/themes/quill_icon_theme.dart'; export 'src/utils/embeds.dart'; -export 'src/utils/extensions/build_context.dart'; export 'src/widgets/controller.dart'; +export 'src/widgets/cursor.dart'; export 'src/widgets/default_styles.dart'; export 'src/widgets/editor/editor.dart'; export 'src/widgets/embeds.dart'; export 'src/widgets/link.dart' show LinkActionPickerDelegate, LinkMenuAction; +export 'src/widgets/raw_editor/raw_editor.dart'; +export 'src/widgets/raw_editor/raw_editor_state.dart'; export 'src/widgets/style_widgets/style_widgets.dart'; export 'src/widgets/toolbar/base_toolbar.dart'; export 'src/widgets/toolbar/toolbar.dart'; diff --git a/lib/src/core/utils/logger.dart b/lib/src/core/utils/logger.dart new file mode 100644 index 00000000..a0c5c805 --- /dev/null +++ b/lib/src/core/utils/logger.dart @@ -0,0 +1,68 @@ +import 'dart:async' show Zone; +import 'dart:developer' as dev show log; + +import 'package:flutter/foundation.dart' show kDebugMode; +import 'package:meta/meta.dart' show immutable; + +/// Simple logger for the quill libraries +/// +/// it log only if [kDebugMode] is true +/// so only for development mode and not in production +/// +@immutable +class QuillLogger { + const QuillLogger._(); + + static bool shouldLog() { + return kDebugMode; + } + + static void log( + T message, { + DateTime? time, + int? sequenceNumber, + int level = 0, + String name = '', + Zone? zone, + StackTrace? stackTrace, + }) { + if (!shouldLog()) { + return; + } + dev.log( + message.toString(), + time: time, + sequenceNumber: sequenceNumber, + level: level, + name: name, + zone: zone, + stackTrace: stackTrace, + ); + } + + static void error( + T message, { + DateTime? time, + int? sequenceNumber, + int level = 0, + String name = '', + Zone? zone, + Object? error, + StackTrace? stackTrace, + }) { + if (!shouldLog()) { + return; + } + + dev.log( + message.toString(), + time: time, + sequenceNumber: sequenceNumber, + level: level, + name: name, + zone: zone, + error: error, + stackTrace: stackTrace, + ); + } +} diff --git a/lib/src/utils/extensions/quill_controller.dart b/lib/src/extensions/quill_controller.dart similarity index 82% rename from lib/src/utils/extensions/quill_controller.dart rename to lib/src/extensions/quill_controller.dart index 6fa3c73b..ca7c3699 100644 --- a/lib/src/utils/extensions/quill_controller.dart +++ b/lib/src/extensions/quill_controller.dart @@ -1,7 +1,7 @@ import 'package:flutter/widgets.dart' show BuildContext; -import '../../../flutter_quill.dart' show QuillController, QuillProvider; -import 'build_context.dart'; +import '../../flutter_quill.dart' show QuillController, QuillProvider; +import 'quill_provider.dart'; extension QuillControllerNullableExt on QuillController? { /// Simple logic to use the current passed controller if not null diff --git a/lib/src/utils/extensions/build_context.dart b/lib/src/extensions/quill_provider.dart similarity index 97% rename from lib/src/utils/extensions/build_context.dart rename to lib/src/extensions/quill_provider.dart index 636b3e1c..b9089943 100644 --- a/lib/src/utils/extensions/build_context.dart +++ b/lib/src/extensions/quill_provider.dart @@ -1,12 +1,11 @@ import 'package:flutter/widgets.dart' show BuildContext; -import '../../../flutter_quill.dart'; +import '../../flutter_quill.dart'; -// TODO: The documentation of this file needs to -//be updated as it's quite oudated. +// TODO: The comments of this file is outdated and needs to be updated /// Public shared extension -extension BuildContextExt on BuildContext { +extension QuillProviderExt on BuildContext { /// return [QuillProvider] as not null /// throw exception if it's not in the widget tree QuillProvider get requireQuillProvider { diff --git a/lib/src/l10n/extensions/localizations.dart b/lib/src/l10n/extensions/localizations.dart new file mode 100644 index 00000000..dea6be89 --- /dev/null +++ b/lib/src/l10n/extensions/localizations.dart @@ -0,0 +1,21 @@ +import 'package:flutter/widgets.dart' show BuildContext; + +import '../generated/quill_localizations.dart' as generated; + +typedef FlutterQuillLocalizations = generated.FlutterQuillLocalizations; + +extension LocalizationsExt on BuildContext { + /// Require the [FlutterQuillLocalizations] instance + /// + /// `loc` is short for `localizations` + FlutterQuillLocalizations get loc { + return FlutterQuillLocalizations.of(this) ?? + (throw UnimplementedError( + "The instance of FlutterQuillLocalizations.of(context) is null and it's" + ' required, please make sure you wrapping the current widget with ' + 'FlutterQuillLocalizationsWidget or add ' + 'FlutterQuillLocalizations.delegate to the localizationsDelegates ' + 'in your App widget, please consider report this in GitHub as a bug', + )); + } +} diff --git a/lib/src/l10n/generated/quill_localizations.dart b/lib/src/l10n/generated/quill_localizations.dart new file mode 100644 index 00000000..756ac2c9 --- /dev/null +++ b/lib/src/l10n/generated/quill_localizations.dart @@ -0,0 +1,753 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:intl/intl.dart' as intl; + +import 'quill_localizations_ar.dart'; +import 'quill_localizations_bg.dart'; +import 'quill_localizations_bn.dart'; +import 'quill_localizations_cs.dart'; +import 'quill_localizations_da.dart'; +import 'quill_localizations_de.dart'; +import 'quill_localizations_en.dart'; +import 'quill_localizations_es.dart'; +import 'quill_localizations_fa.dart'; +import 'quill_localizations_fr.dart'; +import 'quill_localizations_he.dart'; +import 'quill_localizations_hi.dart'; +import 'quill_localizations_id.dart'; +import 'quill_localizations_it.dart'; +import 'quill_localizations_ja.dart'; +import 'quill_localizations_ko.dart'; +import 'quill_localizations_ms.dart'; +import 'quill_localizations_nl.dart'; +import 'quill_localizations_no.dart'; +import 'quill_localizations_pl.dart'; +import 'quill_localizations_pt.dart'; +import 'quill_localizations_ru.dart'; +import 'quill_localizations_sr.dart'; +import 'quill_localizations_sw.dart'; +import 'quill_localizations_tk.dart'; +import 'quill_localizations_tr.dart'; +import 'quill_localizations_uk.dart'; +import 'quill_localizations_ur.dart'; +import 'quill_localizations_vi.dart'; +import 'quill_localizations_zh.dart'; + +/// Callers can lookup localized strings with an instance of FlutterQuillLocalizations +/// returned by `FlutterQuillLocalizations.of(context)`. +/// +/// Applications need to include `FlutterQuillLocalizations.delegate()` in their app's +/// `localizationDelegates` list, and the locales they support in the app's +/// `supportedLocales` list. For example: +/// +/// ```dart +/// import 'generated/quill_localizations.dart'; +/// +/// return MaterialApp( +/// localizationsDelegates: FlutterQuillLocalizations.localizationsDelegates, +/// supportedLocales: FlutterQuillLocalizations.supportedLocales, +/// home: MyApplicationHome(), +/// ); +/// ``` +/// +/// ## Update pubspec.yaml +/// +/// Please make sure to update your pubspec.yaml to include the following +/// packages: +/// +/// ```yaml +/// dependencies: +/// # Internationalization support. +/// flutter_localizations: +/// sdk: flutter +/// intl: any # Use the pinned version from flutter_localizations +/// +/// # Rest of dependencies +/// ``` +/// +/// ## iOS Applications +/// +/// iOS applications define key application metadata, including supported +/// locales, in an Info.plist file that is built into the application bundle. +/// To configure the locales supported by your app, you’ll need to edit this +/// file. +/// +/// First, open your project’s ios/Runner.xcworkspace Xcode workspace file. +/// Then, in the Project Navigator, open the Info.plist file under the Runner +/// project’s Runner folder. +/// +/// Next, select the Information Property List item, select Add Item from the +/// Editor menu, then select Localizations from the pop-up menu. +/// +/// Select and expand the newly-created Localizations item then, for each +/// locale your application supports, add a new item and select the locale +/// you wish to add from the pop-up menu in the Value field. This list should +/// be consistent with the languages listed in the FlutterQuillLocalizations.supportedLocales +/// property. +abstract class FlutterQuillLocalizations { + FlutterQuillLocalizations(String locale) + : localeName = intl.Intl.canonicalizedLocale(locale.toString()); + + final String localeName; + + static FlutterQuillLocalizations? of(BuildContext context) { + return Localizations.of( + context, FlutterQuillLocalizations); + } + + static const LocalizationsDelegate delegate = + _FlutterQuillLocalizationsDelegate(); + + /// A list of this localizations delegate along with the default localizations + /// delegates. + /// + /// Returns a list of localizations delegates containing this delegate along with + /// GlobalMaterialLocalizations.delegate, GlobalCupertinoLocalizations.delegate, + /// and GlobalWidgetsLocalizations.delegate. + /// + /// Additional delegates can be added by appending to this list in + /// MaterialApp. This list does not have to be used at all if a custom list + /// of delegates is preferred or required. + static const List> localizationsDelegates = + >[ + delegate, + GlobalMaterialLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + ]; + + /// A list of this localizations delegate's supported locales. + static const List supportedLocales = [ + Locale('ar'), + Locale('bg'), + Locale('bn'), + Locale('cs'), + Locale('da'), + Locale('de'), + Locale('en'), + Locale('en', 'US'), + Locale('es'), + Locale('fa'), + Locale('fr'), + Locale('he'), + Locale('hi'), + Locale('id'), + Locale('it'), + Locale('ja'), + Locale('ko'), + Locale('ms'), + Locale('nl'), + Locale('no'), + Locale('pl'), + Locale('pt'), + Locale('pt', 'BR'), + Locale('ru'), + Locale('sr'), + Locale('sw'), + Locale('tk'), + Locale('tr'), + Locale('uk'), + Locale('ur'), + Locale('vi'), + Locale('zh'), + Locale('zh', 'CN'), + Locale('zh', 'HK') + ]; + + /// No description provided for @pasteLink. + /// + /// In en, this message translates to: + /// **'Paste a link'** + String get pasteLink; + + /// No description provided for @ok. + /// + /// In en, this message translates to: + /// **'Ok'** + String get ok; + + /// No description provided for @selectColor. + /// + /// In en, this message translates to: + /// **'Select Color'** + String get selectColor; + + /// No description provided for @gallery. + /// + /// In en, this message translates to: + /// **'Gallery'** + String get gallery; + + /// No description provided for @link. + /// + /// In en, this message translates to: + /// **'Link'** + String get link; + + /// No description provided for @open. + /// + /// In en, this message translates to: + /// **'Open'** + String get open; + + /// No description provided for @copy. + /// + /// In en, this message translates to: + /// **'Copy'** + String get copy; + + /// No description provided for @remove. + /// + /// In en, this message translates to: + /// **'Remove'** + String get remove; + + /// No description provided for @save. + /// + /// In en, this message translates to: + /// **'Save'** + String get save; + + /// No description provided for @zoom. + /// + /// In en, this message translates to: + /// **'Zoom'** + String get zoom; + + /// No description provided for @saved. + /// + /// In en, this message translates to: + /// **'Saved'** + String get saved; + + /// No description provided for @text. + /// + /// In en, this message translates to: + /// **'Text'** + String get text; + + /// No description provided for @resize. + /// + /// In en, this message translates to: + /// **'Resize'** + String get resize; + + /// No description provided for @width. + /// + /// In en, this message translates to: + /// **'Width'** + String get width; + + /// No description provided for @height. + /// + /// In en, this message translates to: + /// **'Height'** + String get height; + + /// No description provided for @size. + /// + /// In en, this message translates to: + /// **'Size'** + String get size; + + /// No description provided for @small. + /// + /// In en, this message translates to: + /// **'Small'** + String get small; + + /// No description provided for @large. + /// + /// In en, this message translates to: + /// **'Large'** + String get large; + + /// No description provided for @huge. + /// + /// In en, this message translates to: + /// **'Huge'** + String get huge; + + /// No description provided for @clear. + /// + /// In en, this message translates to: + /// **'Clear'** + String get clear; + + /// No description provided for @font. + /// + /// In en, this message translates to: + /// **'Font'** + String get font; + + /// No description provided for @search. + /// + /// In en, this message translates to: + /// **'Search'** + String get search; + + /// No description provided for @camera. + /// + /// In en, this message translates to: + /// **'Camera'** + String get camera; + + /// No description provided for @video. + /// + /// In en, this message translates to: + /// **'Video'** + String get video; + + /// No description provided for @undo. + /// + /// In en, this message translates to: + /// **'Undo'** + String get undo; + + /// No description provided for @redo. + /// + /// In en, this message translates to: + /// **'Redo'** + String get redo; + + /// No description provided for @fontFamily. + /// + /// In en, this message translates to: + /// **'Font family'** + String get fontFamily; + + /// No description provided for @fontSize. + /// + /// In en, this message translates to: + /// **'Font size'** + String get fontSize; + + /// No description provided for @bold. + /// + /// In en, this message translates to: + /// **'Bold'** + String get bold; + + /// No description provided for @subscript. + /// + /// In en, this message translates to: + /// **'Subscript'** + String get subscript; + + /// No description provided for @superscript. + /// + /// In en, this message translates to: + /// **'Superscript'** + String get superscript; + + /// No description provided for @italic. + /// + /// In en, this message translates to: + /// **'Italic'** + String get italic; + + /// No description provided for @underline. + /// + /// In en, this message translates to: + /// **'Underline'** + String get underline; + + /// No description provided for @strikeThrough. + /// + /// In en, this message translates to: + /// **'Strike through'** + String get strikeThrough; + + /// No description provided for @inlineCode. + /// + /// In en, this message translates to: + /// **'Inline code'** + String get inlineCode; + + /// No description provided for @fontColor. + /// + /// In en, this message translates to: + /// **'Font color'** + String get fontColor; + + /// No description provided for @backgroundColor. + /// + /// In en, this message translates to: + /// **'Background color'** + String get backgroundColor; + + /// No description provided for @clearFormat. + /// + /// In en, this message translates to: + /// **'Clear format'** + String get clearFormat; + + /// No description provided for @alignLeft. + /// + /// In en, this message translates to: + /// **'Align left'** + String get alignLeft; + + /// No description provided for @alignCenter. + /// + /// In en, this message translates to: + /// **'Align center'** + String get alignCenter; + + /// No description provided for @alignRight. + /// + /// In en, this message translates to: + /// **'Align right'** + String get alignRight; + + /// No description provided for @justifyWinWidth. + /// + /// In en, this message translates to: + /// **'Justify win width'** + String get justifyWinWidth; + + /// No description provided for @textDirection. + /// + /// In en, this message translates to: + /// **'Text direction'** + String get textDirection; + + /// No description provided for @headerStyle. + /// + /// In en, this message translates to: + /// **'Header style'** + String get headerStyle; + + /// No description provided for @numberedList. + /// + /// In en, this message translates to: + /// **'Numbered list'** + String get numberedList; + + /// No description provided for @bulletList. + /// + /// In en, this message translates to: + /// **'Bullet list'** + String get bulletList; + + /// No description provided for @checkedList. + /// + /// In en, this message translates to: + /// **'Checked list'** + String get checkedList; + + /// No description provided for @codeBlock. + /// + /// In en, this message translates to: + /// **'Code block'** + String get codeBlock; + + /// No description provided for @quote. + /// + /// In en, this message translates to: + /// **'Quote'** + String get quote; + + /// No description provided for @increaseIndent. + /// + /// In en, this message translates to: + /// **'Increase indent'** + String get increaseIndent; + + /// No description provided for @decreaseIndent. + /// + /// In en, this message translates to: + /// **'Decrease indent'** + String get decreaseIndent; + + /// No description provided for @insertURL. + /// + /// In en, this message translates to: + /// **'Insert URL'** + String get insertURL; + + /// No description provided for @visitLink. + /// + /// In en, this message translates to: + /// **'Visit link'** + String get visitLink; + + /// No description provided for @enterLink. + /// + /// In en, this message translates to: + /// **'Enter link'** + String get enterLink; + + /// No description provided for @enterMedia. + /// + /// In en, this message translates to: + /// **'Enter media'** + String get enterMedia; + + /// No description provided for @edit. + /// + /// In en, this message translates to: + /// **'Edit'** + String get edit; + + /// No description provided for @apply. + /// + /// In en, this message translates to: + /// **'Apply'** + String get apply; + + /// No description provided for @hex. + /// + /// In en, this message translates to: + /// **'Hex'** + String get hex; + + /// No description provided for @material. + /// + /// In en, this message translates to: + /// **'Material'** + String get material; + + /// No description provided for @color. + /// + /// In en, this message translates to: + /// **'Color'** + String get color; + + /// No description provided for @findText. + /// + /// In en, this message translates to: + /// **'Find text'** + String get findText; + + /// No description provided for @moveToPreviousOccurrence. + /// + /// In en, this message translates to: + /// **'Move to previous occurrence'** + String get moveToPreviousOccurrence; + + /// No description provided for @moveToNextOccurrence. + /// + /// In en, this message translates to: + /// **'Move to next occurrence'** + String get moveToNextOccurrence; + + /// No description provided for @savedUsingTheNetwork. + /// + /// In en, this message translates to: + /// **'Saved using the network'** + String get savedUsingTheNetwork; + + /// No description provided for @savedUsingLocalStorage. + /// + /// In en, this message translates to: + /// **'Saved using the local storage'** + String get savedUsingLocalStorage; + + /// No description provided for @errorWhileSavingImage. + /// + /// In en, this message translates to: + /// **'Error while saving image'** + String get errorWhileSavingImage; + + /// No description provided for @pleaseEnterTextForYourLink. + /// + /// In en, this message translates to: + /// **'Please enter a text for your link (e.g., \'Learn more\')'** + String get pleaseEnterTextForYourLink; + + /// No description provided for @pleaseEnterTheLinkURL. + /// + /// In en, this message translates to: + /// **'Please enter the link URL (e.g., \'https://example.com\')'** + String get pleaseEnterTheLinkURL; + + /// No description provided for @pleaseEnterAValidImageURL. + /// + /// In en, this message translates to: + /// **'Please enter a valid image URL'** + String get pleaseEnterAValidImageURL; + + /// No description provided for @pleaseEnterAValidVideoURL. + /// + /// In en, this message translates to: + /// **'Please enter a valid video url'** + String get pleaseEnterAValidVideoURL; + + /// No description provided for @photo. + /// + /// In en, this message translates to: + /// **'Photo'** + String get photo; + + /// No description provided for @image. + /// + /// In en, this message translates to: + /// **'Image'** + String get image; + + /// No description provided for @caseSensitivityAndWholeWordSearch. + /// + /// In en, this message translates to: + /// **'Case sensitivity and whole word search'** + String get caseSensitivityAndWholeWordSearch; + + /// No description provided for @insertImage. + /// + /// In en, this message translates to: + /// **'Insert image'** + String get insertImage; +} + +class _FlutterQuillLocalizationsDelegate + extends LocalizationsDelegate { + const _FlutterQuillLocalizationsDelegate(); + + @override + Future load(Locale locale) { + return SynchronousFuture( + lookupFlutterQuillLocalizations(locale)); + } + + @override + bool isSupported(Locale locale) => [ + 'ar', + 'bg', + 'bn', + 'cs', + 'da', + 'de', + 'en', + 'es', + 'fa', + 'fr', + 'he', + 'hi', + 'id', + 'it', + 'ja', + 'ko', + 'ms', + 'nl', + 'no', + 'pl', + 'pt', + 'ru', + 'sr', + 'sw', + 'tk', + 'tr', + 'uk', + 'ur', + 'vi', + 'zh' + ].contains(locale.languageCode); + + @override + bool shouldReload(_FlutterQuillLocalizationsDelegate old) => false; +} + +FlutterQuillLocalizations lookupFlutterQuillLocalizations(Locale locale) { + // Lookup logic when language+country codes are specified. + switch (locale.languageCode) { + case 'en': + { + switch (locale.countryCode) { + case 'US': + return FlutterQuillLocalizationsEnUs(); + } + break; + } + case 'pt': + { + switch (locale.countryCode) { + case 'BR': + return FlutterQuillLocalizationsPtBr(); + } + break; + } + case 'zh': + { + switch (locale.countryCode) { + case 'CN': + return FlutterQuillLocalizationsZhCn(); + case 'HK': + return FlutterQuillLocalizationsZhHk(); + } + break; + } + } + + // Lookup logic when only language code is specified. + switch (locale.languageCode) { + case 'ar': + return FlutterQuillLocalizationsAr(); + case 'bg': + return FlutterQuillLocalizationsBg(); + case 'bn': + return FlutterQuillLocalizationsBn(); + case 'cs': + return FlutterQuillLocalizationsCs(); + case 'da': + return FlutterQuillLocalizationsDa(); + case 'de': + return FlutterQuillLocalizationsDe(); + case 'en': + return FlutterQuillLocalizationsEn(); + case 'es': + return FlutterQuillLocalizationsEs(); + case 'fa': + return FlutterQuillLocalizationsFa(); + case 'fr': + return FlutterQuillLocalizationsFr(); + case 'he': + return FlutterQuillLocalizationsHe(); + case 'hi': + return FlutterQuillLocalizationsHi(); + case 'id': + return FlutterQuillLocalizationsId(); + case 'it': + return FlutterQuillLocalizationsIt(); + case 'ja': + return FlutterQuillLocalizationsJa(); + case 'ko': + return FlutterQuillLocalizationsKo(); + case 'ms': + return FlutterQuillLocalizationsMs(); + case 'nl': + return FlutterQuillLocalizationsNl(); + case 'no': + return FlutterQuillLocalizationsNo(); + case 'pl': + return FlutterQuillLocalizationsPl(); + case 'pt': + return FlutterQuillLocalizationsPt(); + case 'ru': + return FlutterQuillLocalizationsRu(); + case 'sr': + return FlutterQuillLocalizationsSr(); + case 'sw': + return FlutterQuillLocalizationsSw(); + case 'tk': + return FlutterQuillLocalizationsTk(); + case 'tr': + return FlutterQuillLocalizationsTr(); + case 'uk': + return FlutterQuillLocalizationsUk(); + case 'ur': + return FlutterQuillLocalizationsUr(); + case 'vi': + return FlutterQuillLocalizationsVi(); + case 'zh': + return FlutterQuillLocalizationsZh(); + } + + throw FlutterError( + 'FlutterQuillLocalizations.delegate failed to load unsupported locale "$locale". This is likely ' + 'an issue with the localizations generation tool. Please file an issue ' + 'on GitHub with a reproducible sample app and the gen-l10n configuration ' + 'that was used.'); +} diff --git a/lib/src/l10n/generated/quill_localizations_ar.dart b/lib/src/l10n/generated/quill_localizations_ar.dart new file mode 100644 index 00000000..d78d815a --- /dev/null +++ b/lib/src/l10n/generated/quill_localizations_ar.dart @@ -0,0 +1,229 @@ +import 'quill_localizations.dart'; + +/// The translations for Arabic (`ar`). +class FlutterQuillLocalizationsAr extends FlutterQuillLocalizations { + FlutterQuillLocalizationsAr([super.locale = 'ar']); + + @override + String get pasteLink => 'نسخ الرابط'; + + @override + String get ok => 'نعم'; + + @override + String get selectColor => 'اختار اللون'; + + @override + String get gallery => 'المعرض'; + + @override + String get link => 'الرابط'; + + @override + String get open => 'فتح'; + + @override + String get copy => 'نسخ'; + + @override + String get remove => 'إزالة'; + + @override + String get save => 'حفظ'; + + @override + String get zoom => 'تكبير'; + + @override + String get saved => 'تم الحفظ'; + + @override + String get text => 'نص'; + + @override + String get resize => 'تحجيم'; + + @override + String get width => 'عرض'; + + @override + String get height => 'ارتفاع'; + + @override + String get size => 'حجم'; + + @override + String get small => 'صغير'; + + @override + String get large => 'كبير'; + + @override + String get huge => 'ضخم'; + + @override + String get clear => 'تنظيف'; + + @override + String get font => 'خط'; + + @override + String get search => 'بحث'; + + @override + String get camera => 'كاميرا'; + + @override + String get video => 'فيديو'; + + @override + String get undo => 'تراجع'; + + @override + String get redo => 'تقدم'; + + @override + String get fontFamily => 'عائلة الخط'; + + @override + String get fontSize => 'حجم الخط'; + + @override + String get bold => 'عريض'; + + @override + String get subscript => 'نص سفلي'; + + @override + String get superscript => 'نص علوي'; + + @override + String get italic => 'مائل'; + + @override + String get underline => 'تحته خط'; + + @override + String get strikeThrough => 'داخله خط'; + + @override + String get inlineCode => 'كود بوسط السطر'; + + @override + String get fontColor => 'لون الخط'; + + @override + String get backgroundColor => 'لون الخلفية'; + + @override + String get clearFormat => 'تنظيف التنسيق'; + + @override + String get alignLeft => 'محاذاة اليسار'; + + @override + String get alignCenter => 'محاذاة الوسط'; + + @override + String get alignRight => 'محاذاة اليمين'; + + @override + String get justifyWinWidth => 'تبرير مع العرض'; + + @override + String get textDirection => 'اتجاه النص'; + + @override + String get headerStyle => 'ستايل العنوان'; + + @override + String get numberedList => 'قائمة مرقمة'; + + @override + String get bulletList => 'قائمة منقطة'; + + @override + String get checkedList => 'قائمة للمهام'; + + @override + String get codeBlock => 'كود كامل'; + + @override + String get quote => 'اقتباس'; + + @override + String get increaseIndent => 'زيادة الهامش'; + + @override + String get decreaseIndent => 'تنقيص الهامش'; + + @override + String get insertURL => 'ادخل عنوان رابط'; + + @override + String get visitLink => 'زيارة الرابط'; + + @override + String get enterLink => 'ادخل رابط'; + + @override + String get enterMedia => 'ادخل وسائط'; + + @override + String get edit => 'تعديل'; + + @override + String get apply => 'تطبيق'; + + @override + String get hex => 'Hex'; + + @override + String get material => 'Material'; + + @override + String get color => 'اللون'; + + @override + String get findText => 'بحث عن نص'; + + @override + String get moveToPreviousOccurrence => 'الانتقال إلى الحدث السابق'; + + @override + String get moveToNextOccurrence => 'الانتقال إلى الحدث التالي'; + + @override + String get savedUsingTheNetwork => 'تم الحفظ باستخدام الشبكة'; + + @override + String get savedUsingLocalStorage => 'تم الحفظ باستخدام وحدة التخزين المحلية'; + + @override + String get errorWhileSavingImage => 'حدث خطأ أثناء حفظ الصورة'; + + @override + String get pleaseEnterTextForYourLink => "مثال: 'تعلم المزيد'"; + + @override + String get pleaseEnterTheLinkURL => "مثال: 'https://example.com'"; + + @override + String get pleaseEnterAValidImageURL => 'الرجاء إدخال عنوان URL صحيح للصورة'; + + @override + String get pleaseEnterAValidVideoURL => 'الرجاء إدخال عنوان URL صالح للفيديو'; + + @override + String get photo => 'صورة'; + + @override + String get image => 'صورة'; + + @override + String get caseSensitivityAndWholeWordSearch => + 'حالة الحساسية والبحث عن كلمة كاملة'; + + @override + String get insertImage => 'إدراج صورة'; +} diff --git a/lib/src/l10n/generated/quill_localizations_bg.dart b/lib/src/l10n/generated/quill_localizations_bg.dart new file mode 100644 index 00000000..b0fc467c --- /dev/null +++ b/lib/src/l10n/generated/quill_localizations_bg.dart @@ -0,0 +1,231 @@ +import 'quill_localizations.dart'; + +/// The translations for Bulgarian (`bg`). +class FlutterQuillLocalizationsBg extends FlutterQuillLocalizations { + FlutterQuillLocalizationsBg([super.locale = 'bg']); + + @override + String get pasteLink => 'Поставете връзка'; + + @override + String get ok => 'Да'; + + @override + String get selectColor => 'Изберете цвят'; + + @override + String get gallery => 'Галерия'; + + @override + String get link => 'Връзка'; + + @override + String get open => 'Отвори'; + + @override + String get copy => 'Копирай'; + + @override + String get remove => 'Премахни'; + + @override + String get save => 'Запази'; + + @override + String get zoom => 'Увеличи'; + + @override + String get saved => 'Запазено'; + + @override + String get text => 'Текст'; + + @override + String get resize => 'Промяна на размера'; + + @override + String get width => 'Ширина'; + + @override + String get height => 'Височина'; + + @override + String get size => 'Размер'; + + @override + String get small => 'Малък'; + + @override + String get large => 'Голям'; + + @override + String get huge => 'Огромен'; + + @override + String get clear => 'Изчисти'; + + @override + String get font => 'Шрифт'; + + @override + String get search => 'Търси'; + + @override + String get camera => 'Камера'; + + @override + String get video => 'Видео'; + + @override + String get undo => 'Отмени'; + + @override + String get redo => 'Възстанови'; + + @override + String get fontFamily => 'Шрифт'; + + @override + String get fontSize => 'Размер на шрифта'; + + @override + String get bold => 'Получер'; + + @override + String get subscript => 'Индекс'; + + @override + String get superscript => 'Надпис'; + + @override + String get italic => 'Курсив'; + + @override + String get underline => 'Подчертан'; + + @override + String get strikeThrough => 'Зачертан'; + + @override + String get inlineCode => 'Вграден код'; + + @override + String get fontColor => 'Цвят на шрифта'; + + @override + String get backgroundColor => 'Цвят на фона'; + + @override + String get clearFormat => 'Изчисти формат'; + + @override + String get alignLeft => 'Подравни вляво'; + + @override + String get alignCenter => 'Подравни в центъра'; + + @override + String get alignRight => 'Подравни вдясно'; + + @override + String get justifyWinWidth => 'Подравни във всяка колонка'; + + @override + String get textDirection => 'Посока на текста'; + + @override + String get headerStyle => 'Стил на заглавието'; + + @override + String get numberedList => 'Номериран списък'; + + @override + String get bulletList => 'Маркиран списък'; + + @override + String get checkedList => 'Списък с отметки'; + + @override + String get codeBlock => 'Блок с код'; + + @override + String get quote => 'Цитат'; + + @override + String get increaseIndent => 'Увеличи отстъпа'; + + @override + String get decreaseIndent => 'Намали отстъпа'; + + @override + String get insertURL => 'Вмъкни URL'; + + @override + String get visitLink => 'Посети връзка'; + + @override + String get enterLink => 'Въведи връзка'; + + @override + String get enterMedia => 'Въведи медия'; + + @override + String get edit => 'Редактирай'; + + @override + String get apply => 'Приложи'; + + @override + String get hex => 'Hex'; + + @override + String get material => 'Material'; + + @override + String get color => 'Цвят'; + + @override + String get findText => 'Намери текст'; + + @override + String get moveToPreviousOccurrence => 'Премести към предишното съвпадение'; + + @override + String get moveToNextOccurrence => 'Премести към следващото съвпадение'; + + @override + String get savedUsingTheNetwork => 'Запазено с помощта на мрежата'; + + @override + String get savedUsingLocalStorage => 'Saved using the local storage'; + + @override + String get errorWhileSavingImage => 'Error while saving image'; + + @override + String get pleaseEnterTextForYourLink => "Например, 'Научете повече'"; + + @override + String get pleaseEnterTheLinkURL => "Например, 'https://example.com'"; + + @override + String get pleaseEnterAValidImageURL => + 'Моля, въведете валиден URL на изображението'; + + @override + String get pleaseEnterAValidVideoURL => + 'Моля, въведете валиден URL адрес за видео'; + + @override + String get photo => 'Снимка'; + + @override + String get image => 'Изображение'; + + @override + String get caseSensitivityAndWholeWordSearch => + 'Чувствителност на кутията и търсене на цялата дума'; + + @override + String get insertImage => 'Вмъкване на изображение'; +} diff --git a/lib/src/l10n/generated/quill_localizations_bn.dart b/lib/src/l10n/generated/quill_localizations_bn.dart new file mode 100644 index 00000000..eb8b0f56 --- /dev/null +++ b/lib/src/l10n/generated/quill_localizations_bn.dart @@ -0,0 +1,231 @@ +import 'quill_localizations.dart'; + +/// The translations for Bengali Bangla (`bn`). +class FlutterQuillLocalizationsBn extends FlutterQuillLocalizations { + FlutterQuillLocalizationsBn([super.locale = 'bn']); + + @override + String get pasteLink => 'লিঙ্ক পেস্ট করুন'; + + @override + String get ok => 'ওকে'; + + @override + String get selectColor => 'কালার সিলেক্ট করুন'; + + @override + String get gallery => 'গ্যালারি'; + + @override + String get link => 'লিঙ্ক'; + + @override + String get open => 'ওপেন'; + + @override + String get copy => 'কপি'; + + @override + String get remove => 'রিমুভ'; + + @override + String get save => 'সেভ'; + + @override + String get zoom => 'জুম'; + + @override + String get saved => 'সেভড'; + + @override + String get text => 'টেক্সট'; + + @override + String get resize => 'রিসাইজ'; + + @override + String get width => 'প্রস্থ'; + + @override + String get height => 'দৈর্ঘ্য'; + + @override + String get size => 'সাইজ'; + + @override + String get small => 'ছোট'; + + @override + String get large => 'বড়'; + + @override + String get huge => 'বিশাল'; + + @override + String get clear => 'ক্লিয়ার'; + + @override + String get font => 'ফন্ট'; + + @override + String get search => 'সার্চ'; + + @override + String get camera => 'ক্যামেরা'; + + @override + String get video => 'ভিডিও'; + + @override + String get undo => 'আন্ডু'; + + @override + String get redo => 'রিডু'; + + @override + String get fontFamily => 'ফন্ট ফ্যামিলি'; + + @override + String get fontSize => 'ফন্ট সাইজ'; + + @override + String get bold => 'বোল্ড'; + + @override + String get subscript => 'সাবস্ক্রিপ্ট'; + + @override + String get superscript => 'সুপারস্ক্রিপ্ট'; + + @override + String get italic => 'ইটালিক'; + + @override + String get underline => 'আন্ডারলাইন'; + + @override + String get strikeThrough => 'স্ট্রাইক থ্রু'; + + @override + String get inlineCode => 'ইনলাইন কোড'; + + @override + String get fontColor => 'ফন্ট কালার'; + + @override + String get backgroundColor => 'ব্যাকগ্রাউন্ড কালার'; + + @override + String get clearFormat => 'ক্লিয়ার ফরম্যাট'; + + @override + String get alignLeft => 'বাম সারিবদ্ধ'; + + @override + String get alignCenter => 'কেন্দ্র সারিবদ্ধ'; + + @override + String get alignRight => 'ডান সারিবদ্ধ'; + + @override + String get justifyWinWidth => 'প্রস্থের সাথে সংযত'; + + @override + String get textDirection => 'টেক্সট ডিরেকশন'; + + @override + String get headerStyle => 'হেডার স্টাইল'; + + @override + String get numberedList => 'সংখ্যাযুক্ত তালিকা'; + + @override + String get bulletList => 'বুলেট তালিকা'; + + @override + String get checkedList => 'চেক করা তালিকা'; + + @override + String get codeBlock => 'কোড ব্লক'; + + @override + String get quote => 'উক্তি'; + + @override + String get increaseIndent => 'ইন্ডেন্ট বাড়ান'; + + @override + String get decreaseIndent => 'ইন্ডেন্ট কমান'; + + @override + String get insertURL => 'UR দিন'; + + @override + String get visitLink => 'ভিজিট লিঙ্ক'; + + @override + String get enterLink => 'লিঙ্ক দিন'; + + @override + String get enterMedia => 'মিডিয়া দিন'; + + @override + String get edit => 'ইডিট'; + + @override + String get apply => 'এপ্লাই'; + + @override + String get hex => 'হেক্স'; + + @override + String get material => 'ম্যাটারিয়াল'; + + @override + String get color => 'কালার'; + + @override + String get findText => 'পাঠ্য খুঁজুন'; + + @override + String get moveToPreviousOccurrence => 'পূর্ববর্তী ঘটনায় চলুন'; + + @override + String get moveToNextOccurrence => 'পরবর্তী ঘটনায় চলুন'; + + @override + String get savedUsingTheNetwork => 'নেটওয়ার্ক ব্যবহার করে সংরক্ষিত'; + + @override + String get savedUsingLocalStorage => 'স্থানীয় সংরক্ষণ ব্যবহার করে সংরক্ষিত'; + + @override + String get errorWhileSavingImage => 'চিত্র সংরক্ষণে সময়ে ত্রুটি'; + + @override + String get pleaseEnterTextForYourLink => + "আপনার লিঙ্কের জন্য একটি টেক্সট লিখুন (উদাঃ 'আরও জানুন')"; + + @override + String get pleaseEnterTheLinkURL => + "দয়া করে লিঙ্ক URL লিখুন (উদাঃ 'https://example.com')"; + + @override + String get pleaseEnterAValidImageURL => 'দয়া করে একটি বৈধ চিত্র URL লিখুন'; + + @override + String get pleaseEnterAValidVideoURL => 'দয়া করে একটি বৈধ ভিডিও URL লিখুন'; + + @override + String get photo => 'ফটো'; + + @override + String get image => 'চিত্র'; + + @override + String get caseSensitivityAndWholeWordSearch => + 'কেস সেন্সিটিভিটি এবং পূর্ণ শব্দ অনুসন্ধান'; + + @override + String get insertImage => 'চিত্র সন্নিবেশ'; +} diff --git a/lib/src/l10n/generated/quill_localizations_cs.dart b/lib/src/l10n/generated/quill_localizations_cs.dart new file mode 100644 index 00000000..2c37d03f --- /dev/null +++ b/lib/src/l10n/generated/quill_localizations_cs.dart @@ -0,0 +1,231 @@ +import 'quill_localizations.dart'; + +/// The translations for Czech (`cs`). +class FlutterQuillLocalizationsCs extends FlutterQuillLocalizations { + FlutterQuillLocalizationsCs([super.locale = 'cs']); + + @override + String get pasteLink => 'Vložit odkaz'; + + @override + String get ok => 'Ok'; + + @override + String get selectColor => 'Vybrat barvu'; + + @override + String get gallery => 'Galerie'; + + @override + String get link => 'Odkaz'; + + @override + String get open => 'Otevřít'; + + @override + String get copy => 'Kopírovat'; + + @override + String get remove => 'Odstranit'; + + @override + String get save => 'Uložit'; + + @override + String get zoom => 'Přiblížit'; + + @override + String get saved => 'Uloženo'; + + @override + String get text => 'Text'; + + @override + String get resize => 'Změnit velikost'; + + @override + String get width => 'Šířka'; + + @override + String get height => 'Výška'; + + @override + String get size => 'Velikost'; + + @override + String get small => 'Malý'; + + @override + String get large => 'Velký'; + + @override + String get huge => 'Obrovský'; + + @override + String get clear => 'Smazat'; + + @override + String get font => 'Písmo'; + + @override + String get search => 'Hledat'; + + @override + String get camera => 'Kamera'; + + @override + String get video => 'Video'; + + @override + String get undo => 'Zpět'; + + @override + String get redo => 'Znovu'; + + @override + String get fontFamily => 'Rodina písma'; + + @override + String get fontSize => 'Velikost písma'; + + @override + String get bold => 'Tučné'; + + @override + String get subscript => 'Dolní index'; + + @override + String get superscript => 'Horní index'; + + @override + String get italic => 'Kurzíva'; + + @override + String get underline => 'Podtržení'; + + @override + String get strikeThrough => 'Přeškrtnuté'; + + @override + String get inlineCode => 'Inline kód'; + + @override + String get fontColor => 'Barva písma'; + + @override + String get backgroundColor => 'Barva pozadí'; + + @override + String get clearFormat => 'Vymazat formátování'; + + @override + String get alignLeft => 'Zarovnat vlevo'; + + @override + String get alignCenter => 'Zarovnat na střed'; + + @override + String get alignRight => 'Zarovnat vpravo'; + + @override + String get justifyWinWidth => 'Zarovnat do bloku'; + + @override + String get textDirection => 'Směr textu'; + + @override + String get headerStyle => 'Styl záhlaví'; + + @override + String get numberedList => 'Číslovaný seznam'; + + @override + String get bulletList => 'Seznam s odrážkami'; + + @override + String get checkedList => 'Seznam s zaškrtávacími políčky'; + + @override + String get codeBlock => 'Blokový kód'; + + @override + String get quote => 'Citace'; + + @override + String get increaseIndent => 'Zvětšit odsazení'; + + @override + String get decreaseIndent => 'Zmenšit odsazení'; + + @override + String get insertURL => 'Vložit URL'; + + @override + String get visitLink => 'Otevřít odkaz'; + + @override + String get enterLink => 'Vložit odkaz'; + + @override + String get enterMedia => 'Vložit média'; + + @override + String get edit => 'Upravit'; + + @override + String get apply => 'Použít'; + + @override + String get hex => 'Hex'; + + @override + String get material => 'Material'; + + @override + String get color => 'Barva'; + + @override + String get findText => 'Najít text'; + + @override + String get moveToPreviousOccurrence => 'Přesunout na předchozí výskyt'; + + @override + String get moveToNextOccurrence => 'Přesunout na následující výskyt'; + + @override + String get savedUsingTheNetwork => 'Uloženo pomocí sítě'; + + @override + String get savedUsingLocalStorage => 'Uloženo lokálně'; + + @override + String get errorWhileSavingImage => 'Chyba při ukládání obrázku'; + + @override + String get pleaseEnterTextForYourLink => + "Zadejte text pro váš odkaz (např., 'Dozvědět se více')"; + + @override + String get pleaseEnterTheLinkURL => + "Zadejte URL odkazu (např., 'https://example.com')"; + + @override + String get pleaseEnterAValidImageURL => 'Zadejte platnou URL adresu obrázku'; + + @override + String get pleaseEnterAValidVideoURL => 'Zadejte platnou URL adresu videa'; + + @override + String get photo => 'Foto'; + + @override + String get image => 'Obrázek'; + + @override + String get caseSensitivityAndWholeWordSearch => + 'Citlivost na velká a malá písmena a vyhledávání celého slova'; + + @override + String get insertImage => 'Vložit obrázek'; +} diff --git a/lib/src/l10n/generated/quill_localizations_da.dart b/lib/src/l10n/generated/quill_localizations_da.dart new file mode 100644 index 00000000..4a136af4 --- /dev/null +++ b/lib/src/l10n/generated/quill_localizations_da.dart @@ -0,0 +1,229 @@ +import 'quill_localizations.dart'; + +/// The translations for Danish (`da`). +class FlutterQuillLocalizationsDa extends FlutterQuillLocalizations { + FlutterQuillLocalizationsDa([super.locale = 'da']); + + @override + String get pasteLink => 'Indsæt link'; + + @override + String get ok => 'Ok'; + + @override + String get selectColor => 'Vælg farve'; + + @override + String get gallery => 'Galleri'; + + @override + String get link => 'Link'; + + @override + String get open => 'Åben'; + + @override + String get copy => 'Kopi'; + + @override + String get remove => 'Fjerne'; + + @override + String get save => 'Gemme'; + + @override + String get zoom => 'Zoom ind'; + + @override + String get saved => 'Gemt'; + + @override + String get text => 'Text'; + + @override + String get resize => 'Resize'; + + @override + String get width => 'Width'; + + @override + String get height => 'Height'; + + @override + String get size => 'Size'; + + @override + String get small => 'Small'; + + @override + String get large => 'Large'; + + @override + String get huge => 'Huge'; + + @override + String get clear => 'Clear'; + + @override + String get font => 'Font'; + + @override + String get search => 'Search'; + + @override + String get camera => 'Camera'; + + @override + String get video => 'Video'; + + @override + String get undo => 'Undo'; + + @override + String get redo => 'Redo'; + + @override + String get fontFamily => 'Font family'; + + @override + String get fontSize => 'Font size'; + + @override + String get bold => 'Bold'; + + @override + String get subscript => 'Subscript'; + + @override + String get superscript => 'Superscript'; + + @override + String get italic => 'Italic'; + + @override + String get underline => 'Underline'; + + @override + String get strikeThrough => 'Strike through'; + + @override + String get inlineCode => 'Inline code'; + + @override + String get fontColor => 'Font color'; + + @override + String get backgroundColor => 'Background color'; + + @override + String get clearFormat => 'Clear format'; + + @override + String get alignLeft => 'Align left'; + + @override + String get alignCenter => 'Align center'; + + @override + String get alignRight => 'Align right'; + + @override + String get justifyWinWidth => 'Justify win width'; + + @override + String get textDirection => 'Text direction'; + + @override + String get headerStyle => 'Header style'; + + @override + String get numberedList => 'Numbered list'; + + @override + String get bulletList => 'Bullet list'; + + @override + String get checkedList => 'Checked list'; + + @override + String get codeBlock => 'Code block'; + + @override + String get quote => 'Quote'; + + @override + String get increaseIndent => 'Increase indent'; + + @override + String get decreaseIndent => 'Decrease indent'; + + @override + String get insertURL => 'Insert URL'; + + @override + String get visitLink => 'Visit link'; + + @override + String get enterLink => 'Enter link'; + + @override + String get enterMedia => 'Enter media'; + + @override + String get edit => 'Edit'; + + @override + String get apply => 'Apply'; + + @override + String get hex => 'Hex'; + + @override + String get material => 'Materiale'; + + @override + String get color => 'Farve'; + + @override + String get findText => 'Find text'; + + @override + String get moveToPreviousOccurrence => 'Move to previous occurrence'; + + @override + String get moveToNextOccurrence => 'Move to next occurrence'; + + @override + String get savedUsingTheNetwork => 'Saved using the network'; + + @override + String get savedUsingLocalStorage => 'Saved using the local storage'; + + @override + String get errorWhileSavingImage => 'Error while saving image'; + + @override + String get pleaseEnterTextForYourLink => "e.g., 'Learn more'"; + + @override + String get pleaseEnterTheLinkURL => "e.g., 'https://example.com'"; + + @override + String get pleaseEnterAValidImageURL => 'Please enter a valid image URL'; + + @override + String get pleaseEnterAValidVideoURL => 'Angiv en gyldig video-URL'; + + @override + String get photo => 'Foto'; + + @override + String get image => 'Billede'; + + @override + String get caseSensitivityAndWholeWordSearch => + 'Stor- og småbogstavsfølsomhed samt helordsøgning'; + + @override + String get insertImage => 'Indsæt billede'; +} diff --git a/lib/src/l10n/generated/quill_localizations_de.dart b/lib/src/l10n/generated/quill_localizations_de.dart new file mode 100644 index 00000000..79ae25be --- /dev/null +++ b/lib/src/l10n/generated/quill_localizations_de.dart @@ -0,0 +1,230 @@ +import 'quill_localizations.dart'; + +/// The translations for German (`de`). +class FlutterQuillLocalizationsDe extends FlutterQuillLocalizations { + FlutterQuillLocalizationsDe([super.locale = 'de']); + + @override + String get pasteLink => 'Link hinzufügen'; + + @override + String get ok => 'OK'; + + @override + String get selectColor => 'Farbe auswählen'; + + @override + String get gallery => 'Galerie'; + + @override + String get link => 'Link'; + + @override + String get open => 'Öffnen'; + + @override + String get copy => 'Kopieren'; + + @override + String get remove => 'Entfernen'; + + @override + String get save => 'Speichern'; + + @override + String get zoom => 'Zoomen'; + + @override + String get saved => 'Gespeichert'; + + @override + String get text => 'Text'; + + @override + String get resize => 'Größe ändern'; + + @override + String get width => 'Breite'; + + @override + String get height => 'Höhe'; + + @override + String get size => 'Größe'; + + @override + String get small => 'Klein'; + + @override + String get large => 'Groß'; + + @override + String get huge => 'Riesig'; + + @override + String get clear => 'Löschen'; + + @override + String get font => 'Schrift'; + + @override + String get search => 'Suchen'; + + @override + String get camera => 'Kamera'; + + @override + String get video => 'Video'; + + @override + String get undo => 'Rückgängig'; + + @override + String get redo => 'Wiederherstellen'; + + @override + String get fontFamily => 'Schriftart'; + + @override + String get fontSize => 'Schriftgröße'; + + @override + String get bold => 'Fett'; + + @override + String get subscript => 'Tiefgestellt'; + + @override + String get superscript => 'Hochgestellt'; + + @override + String get italic => 'Kursiv'; + + @override + String get underline => 'Unterstreichen'; + + @override + String get strikeThrough => 'Durchstreichen'; + + @override + String get inlineCode => 'Inline-Code'; + + @override + String get fontColor => 'Schriftfarbe'; + + @override + String get backgroundColor => 'Hintergrundfarbe'; + + @override + String get clearFormat => 'Formatierung löschen'; + + @override + String get alignLeft => 'Linksbündig ausrichten'; + + @override + String get alignCenter => 'Zentriert ausrichten'; + + @override + String get alignRight => 'Rechtsbündig ausrichten'; + + @override + String get justifyWinWidth => 'Blocksatz'; + + @override + String get textDirection => 'Textrichtung'; + + @override + String get headerStyle => 'Überschrift-Stil'; + + @override + String get numberedList => 'Nummerierte Liste'; + + @override + String get bulletList => 'Aufzählungsliste'; + + @override + String get checkedList => 'Checkliste'; + + @override + String get codeBlock => 'Code-Block'; + + @override + String get quote => 'Zitat'; + + @override + String get increaseIndent => 'Einzug vergrößern'; + + @override + String get decreaseIndent => 'Einzug verkleinern'; + + @override + String get insertURL => 'URL einfügen'; + + @override + String get visitLink => 'Link öffnen'; + + @override + String get enterLink => 'Link eingeben'; + + @override + String get enterMedia => 'Medien einfügen'; + + @override + String get edit => 'Bearbeiten'; + + @override + String get apply => 'Anwenden'; + + @override + String get hex => 'Hex'; + + @override + String get material => 'Material'; + + @override + String get color => 'Farbe'; + + @override + String get findText => 'Text suchen'; + + @override + String get moveToPreviousOccurrence => 'Zum vorherigen Auftreten springen'; + + @override + String get moveToNextOccurrence => 'Zum nächsten Auftreten springen'; + + @override + String get savedUsingTheNetwork => 'Saved using the network'; + + @override + String get savedUsingLocalStorage => 'Saved using the local storage'; + + @override + String get errorWhileSavingImage => 'Error while saving image'; + + @override + String get pleaseEnterTextForYourLink => "e.g., 'Learn more'"; + + @override + String get pleaseEnterTheLinkURL => "e.g., 'https://example.com'"; + + @override + String get pleaseEnterAValidImageURL => 'Please enter a valid image URL'; + + @override + String get pleaseEnterAValidVideoURL => + 'Bitte geben Sie eine gültige Video-URL ein'; + + @override + String get photo => 'Foto'; + + @override + String get image => 'Bild'; + + @override + String get caseSensitivityAndWholeWordSearch => + 'Groß- und Kleinschreibung sowie Ganzwortsuche'; + + @override + String get insertImage => 'Bild einfügen'; +} diff --git a/lib/src/l10n/generated/quill_localizations_en.dart b/lib/src/l10n/generated/quill_localizations_en.dart new file mode 100644 index 00000000..c69876d9 --- /dev/null +++ b/lib/src/l10n/generated/quill_localizations_en.dart @@ -0,0 +1,461 @@ +import 'quill_localizations.dart'; + +/// The translations for English (`en`). +class FlutterQuillLocalizationsEn extends FlutterQuillLocalizations { + FlutterQuillLocalizationsEn([super.locale = 'en']); + + @override + String get pasteLink => 'Paste a link'; + + @override + String get ok => 'Ok'; + + @override + String get selectColor => 'Select Color'; + + @override + String get gallery => 'Gallery'; + + @override + String get link => 'Link'; + + @override + String get open => 'Open'; + + @override + String get copy => 'Copy'; + + @override + String get remove => 'Remove'; + + @override + String get save => 'Save'; + + @override + String get zoom => 'Zoom'; + + @override + String get saved => 'Saved'; + + @override + String get text => 'Text'; + + @override + String get resize => 'Resize'; + + @override + String get width => 'Width'; + + @override + String get height => 'Height'; + + @override + String get size => 'Size'; + + @override + String get small => 'Small'; + + @override + String get large => 'Large'; + + @override + String get huge => 'Huge'; + + @override + String get clear => 'Clear'; + + @override + String get font => 'Font'; + + @override + String get search => 'Search'; + + @override + String get camera => 'Camera'; + + @override + String get video => 'Video'; + + @override + String get undo => 'Undo'; + + @override + String get redo => 'Redo'; + + @override + String get fontFamily => 'Font family'; + + @override + String get fontSize => 'Font size'; + + @override + String get bold => 'Bold'; + + @override + String get subscript => 'Subscript'; + + @override + String get superscript => 'Superscript'; + + @override + String get italic => 'Italic'; + + @override + String get underline => 'Underline'; + + @override + String get strikeThrough => 'Strike through'; + + @override + String get inlineCode => 'Inline code'; + + @override + String get fontColor => 'Font color'; + + @override + String get backgroundColor => 'Background color'; + + @override + String get clearFormat => 'Clear format'; + + @override + String get alignLeft => 'Align left'; + + @override + String get alignCenter => 'Align center'; + + @override + String get alignRight => 'Align right'; + + @override + String get justifyWinWidth => 'Justify win width'; + + @override + String get textDirection => 'Text direction'; + + @override + String get headerStyle => 'Header style'; + + @override + String get numberedList => 'Numbered list'; + + @override + String get bulletList => 'Bullet list'; + + @override + String get checkedList => 'Checked list'; + + @override + String get codeBlock => 'Code block'; + + @override + String get quote => 'Quote'; + + @override + String get increaseIndent => 'Increase indent'; + + @override + String get decreaseIndent => 'Decrease indent'; + + @override + String get insertURL => 'Insert URL'; + + @override + String get visitLink => 'Visit link'; + + @override + String get enterLink => 'Enter link'; + + @override + String get enterMedia => 'Enter media'; + + @override + String get edit => 'Edit'; + + @override + String get apply => 'Apply'; + + @override + String get hex => 'Hex'; + + @override + String get material => 'Material'; + + @override + String get color => 'Color'; + + @override + String get findText => 'Find text'; + + @override + String get moveToPreviousOccurrence => 'Move to previous occurrence'; + + @override + String get moveToNextOccurrence => 'Move to next occurrence'; + + @override + String get savedUsingTheNetwork => 'Saved using the network'; + + @override + String get savedUsingLocalStorage => 'Saved using the local storage'; + + @override + String get errorWhileSavingImage => 'Error while saving image'; + + @override + String get pleaseEnterTextForYourLink => + "Please enter a text for your link (e.g., 'Learn more')"; + + @override + String get pleaseEnterTheLinkURL => + "Please enter the link URL (e.g., 'https://example.com')"; + + @override + String get pleaseEnterAValidImageURL => 'Please enter a valid image URL'; + + @override + String get pleaseEnterAValidVideoURL => 'Please enter a valid video url'; + + @override + String get photo => 'Photo'; + + @override + String get image => 'Image'; + + @override + String get caseSensitivityAndWholeWordSearch => + 'Case sensitivity and whole word search'; + + @override + String get insertImage => 'Insert image'; +} + +/// The translations for English, as used in the United States (`en_US`). +class FlutterQuillLocalizationsEnUs extends FlutterQuillLocalizationsEn { + FlutterQuillLocalizationsEnUs() : super('en_US'); + + @override + String get pasteLink => 'Paste a link'; + + @override + String get ok => 'Ok'; + + @override + String get selectColor => 'Select Color'; + + @override + String get gallery => 'Gallery'; + + @override + String get link => 'Link'; + + @override + String get open => 'Open'; + + @override + String get copy => 'Copy'; + + @override + String get remove => 'Remove'; + + @override + String get save => 'Save'; + + @override + String get zoom => 'Zoom'; + + @override + String get saved => 'Saved'; + + @override + String get text => 'Text'; + + @override + String get resize => 'Resize'; + + @override + String get width => 'Width'; + + @override + String get height => 'Height'; + + @override + String get size => 'Size'; + + @override + String get small => 'Small'; + + @override + String get large => 'Large'; + + @override + String get huge => 'Huge'; + + @override + String get clear => 'Clear'; + + @override + String get font => 'Font'; + + @override + String get search => 'Search'; + + @override + String get camera => 'Camera'; + + @override + String get video => 'Video'; + + @override + String get undo => 'Undo'; + + @override + String get redo => 'Redo'; + + @override + String get fontFamily => 'Font family'; + + @override + String get fontSize => 'Font size'; + + @override + String get bold => 'Bold'; + + @override + String get subscript => 'Subscript'; + + @override + String get superscript => 'Superscript'; + + @override + String get italic => 'Italic'; + + @override + String get underline => 'Underline'; + + @override + String get strikeThrough => 'Strike through'; + + @override + String get inlineCode => 'Inline code'; + + @override + String get fontColor => 'Font color'; + + @override + String get backgroundColor => 'Background color'; + + @override + String get clearFormat => 'Clear format'; + + @override + String get alignLeft => 'Align left'; + + @override + String get alignCenter => 'Align center'; + + @override + String get alignRight => 'Align right'; + + @override + String get justifyWinWidth => 'Justify win width'; + + @override + String get textDirection => 'Text direction'; + + @override + String get headerStyle => 'Header style'; + + @override + String get numberedList => 'Numbered list'; + + @override + String get bulletList => 'Bullet list'; + + @override + String get checkedList => 'Checked list'; + + @override + String get codeBlock => 'Code block'; + + @override + String get quote => 'Quote'; + + @override + String get increaseIndent => 'Increase indent'; + + @override + String get decreaseIndent => 'Decrease indent'; + + @override + String get insertURL => 'Insert URL'; + + @override + String get visitLink => 'Visit link'; + + @override + String get enterLink => 'Enter link'; + + @override + String get enterMedia => 'Enter media'; + + @override + String get edit => 'Edit'; + + @override + String get apply => 'Apply'; + + @override + String get hex => 'Hex'; + + @override + String get material => 'Material'; + + @override + String get color => 'Color'; + + @override + String get findText => 'Find text'; + + @override + String get moveToPreviousOccurrence => 'Move to previous occurrence'; + + @override + String get moveToNextOccurrence => 'Move to next occurrence'; + + @override + String get savedUsingTheNetwork => 'Saved using the network'; + + @override + String get savedUsingLocalStorage => 'Saved using the local storage'; + + @override + String get errorWhileSavingImage => 'Error while saving image'; + + @override + String get pleaseEnterTextForYourLink => + "Please enter a text for your link (e.g., 'Learn more')"; + + @override + String get pleaseEnterTheLinkURL => + "Please enter the link URL (e.g., 'https://example.com')"; + + @override + String get pleaseEnterAValidImageURL => 'Please enter a valid image URL'; + + @override + String get pleaseEnterAValidVideoURL => 'Please enter a valid video URL'; + + @override + String get photo => 'Photo'; + + @override + String get image => 'Image'; + + @override + String get caseSensitivityAndWholeWordSearch => + 'Case sensitivity and whole word search'; + + @override + String get insertImage => 'Insert Image'; +} diff --git a/lib/src/l10n/generated/quill_localizations_es.dart b/lib/src/l10n/generated/quill_localizations_es.dart new file mode 100644 index 00000000..85c895f3 --- /dev/null +++ b/lib/src/l10n/generated/quill_localizations_es.dart @@ -0,0 +1,230 @@ +import 'quill_localizations.dart'; + +/// The translations for Spanish Castilian (`es`). +class FlutterQuillLocalizationsEs extends FlutterQuillLocalizations { + FlutterQuillLocalizationsEs([super.locale = 'es']); + + @override + String get pasteLink => 'Pega un enlace'; + + @override + String get ok => 'Ok'; + + @override + String get selectColor => 'Selecciona un color'; + + @override + String get gallery => 'Galería'; + + @override + String get link => 'Enlace'; + + @override + String get open => 'Abrir'; + + @override + String get copy => 'Copiar'; + + @override + String get remove => 'Eliminar'; + + @override + String get save => 'Guardar'; + + @override + String get zoom => 'Zoom'; + + @override + String get saved => 'Guardado'; + + @override + String get text => 'Texto'; + + @override + String get resize => 'Redimensionar'; + + @override + String get width => 'Ancho'; + + @override + String get height => 'Alto'; + + @override + String get size => 'Tamaño'; + + @override + String get small => 'Pequeño'; + + @override + String get large => 'Grande'; + + @override + String get huge => 'Muy grande'; + + @override + String get clear => 'Borrar'; + + @override + String get font => 'Fuente'; + + @override + String get search => 'Buscar'; + + @override + String get camera => 'Cámara'; + + @override + String get video => 'Vídeo'; + + @override + String get undo => 'Undo'; + + @override + String get redo => 'Redo'; + + @override + String get fontFamily => 'Font family'; + + @override + String get fontSize => 'Font size'; + + @override + String get bold => 'Bold'; + + @override + String get subscript => 'Subscript'; + + @override + String get superscript => 'Superscript'; + + @override + String get italic => 'Italic'; + + @override + String get underline => 'Underline'; + + @override + String get strikeThrough => 'Strike through'; + + @override + String get inlineCode => 'Inline code'; + + @override + String get fontColor => 'Font color'; + + @override + String get backgroundColor => 'Background color'; + + @override + String get clearFormat => 'Clear format'; + + @override + String get alignLeft => 'Align left'; + + @override + String get alignCenter => 'Align center'; + + @override + String get alignRight => 'Align right'; + + @override + String get justifyWinWidth => 'Justify win width'; + + @override + String get textDirection => 'Text direction'; + + @override + String get headerStyle => 'Header style'; + + @override + String get numberedList => 'Numbered list'; + + @override + String get bulletList => 'Bullet list'; + + @override + String get checkedList => 'Checked list'; + + @override + String get codeBlock => 'Code block'; + + @override + String get quote => 'Quote'; + + @override + String get increaseIndent => 'Increase indent'; + + @override + String get decreaseIndent => 'Decrease indent'; + + @override + String get insertURL => 'Insert URL'; + + @override + String get visitLink => 'Visit link'; + + @override + String get enterLink => 'Enter link'; + + @override + String get enterMedia => 'Enter media'; + + @override + String get edit => 'Edit'; + + @override + String get apply => 'Apply'; + + @override + String get hex => 'Hex'; + + @override + String get material => 'Material'; + + @override + String get color => 'Color'; + + @override + String get findText => 'Find text'; + + @override + String get moveToPreviousOccurrence => 'Move to previous occurrence'; + + @override + String get moveToNextOccurrence => 'Move to next occurrence'; + + @override + String get savedUsingTheNetwork => 'Saved using the network'; + + @override + String get savedUsingLocalStorage => 'Saved using the local storage'; + + @override + String get errorWhileSavingImage => 'Error while saving image'; + + @override + String get pleaseEnterTextForYourLink => "e.g., 'Learn more'"; + + @override + String get pleaseEnterTheLinkURL => "e.g., 'https://example.com'"; + + @override + String get pleaseEnterAValidImageURL => 'Please enter a valid image URL'; + + @override + String get pleaseEnterAValidVideoURL => + 'Por favor, ingrese una URL de video válida'; + + @override + String get photo => 'Foto'; + + @override + String get image => 'Imagen'; + + @override + String get caseSensitivityAndWholeWordSearch => + 'Sensibilidad a mayúsculas y minúsculas y búsqueda de palabras completas'; + + @override + String get insertImage => 'Insertar imagen'; +} diff --git a/lib/src/l10n/generated/quill_localizations_fa.dart b/lib/src/l10n/generated/quill_localizations_fa.dart new file mode 100644 index 00000000..e649354f --- /dev/null +++ b/lib/src/l10n/generated/quill_localizations_fa.dart @@ -0,0 +1,232 @@ +import 'quill_localizations.dart'; + +/// The translations for Persian (`fa`). +class FlutterQuillLocalizationsFa extends FlutterQuillLocalizations { + FlutterQuillLocalizationsFa([super.locale = 'fa']); + + @override + String get pasteLink => 'جایگذاری لینک'; + + @override + String get ok => 'تایید'; + + @override + String get selectColor => 'انتخاب رنگ'; + + @override + String get gallery => 'گالری'; + + @override + String get link => 'لینک'; + + @override + String get open => 'باز کردن'; + + @override + String get copy => 'کپی'; + + @override + String get remove => 'حذف'; + + @override + String get save => 'ذخیره'; + + @override + String get zoom => 'بزرگنمایی'; + + @override + String get saved => 'ذخیره شد'; + + @override + String get text => 'متن'; + + @override + String get resize => 'تغییر اندازه'; + + @override + String get width => 'عرض'; + + @override + String get height => 'طول'; + + @override + String get size => 'اندازه'; + + @override + String get small => 'کوچک'; + + @override + String get large => 'بزرگ'; + + @override + String get huge => 'خیلی بزرگ'; + + @override + String get clear => 'پاک کردن'; + + @override + String get font => 'فونت'; + + @override + String get search => 'جستجو'; + + @override + String get camera => 'دوربین'; + + @override + String get video => 'ویدیو'; + + @override + String get undo => 'Undo'; + + @override + String get redo => 'Redo'; + + @override + String get fontFamily => 'Sخانواده فونت'; + + @override + String get fontSize => 'اندازه فونت'; + + @override + String get bold => 'توپر'; + + @override + String get subscript => 'زیرنویس'; + + @override + String get superscript => 'بالانویس'; + + @override + String get italic => 'مورب'; + + @override + String get underline => 'زیرخط'; + + @override + String get strikeThrough => 'خط خورده'; + + @override + String get inlineCode => 'کد درون خطی'; + + @override + String get fontColor => 'رنگ فونت'; + + @override + String get backgroundColor => 'رنگ زمینه'; + + @override + String get clearFormat => 'پاکسازی فرمت'; + + @override + String get alignLeft => 'چیدمان چپ'; + + @override + String get alignCenter => 'چیدمان وسط'; + + @override + String get alignRight => 'چیدمان راست'; + + @override + String get justifyWinWidth => 'تضمین عرض پنجره'; + + @override + String get textDirection => 'جهت متن'; + + @override + String get headerStyle => 'سبک هدر'; + + @override + String get numberedList => 'لیست شماره‌دار'; + + @override + String get bulletList => 'لیست نقطه‌ای'; + + @override + String get checkedList => 'لیست با علامت'; + + @override + String get codeBlock => 'بلوک کد'; + + @override + String get quote => 'نقل قول'; + + @override + String get increaseIndent => 'افزایش تورفتگی'; + + @override + String get decreaseIndent => 'کاهش تورفتگی'; + + @override + String get insertURL => 'درج URL'; + + @override + String get visitLink => 'بازدید از لینک'; + + @override + String get enterLink => 'ورود لینک'; + + @override + String get enterMedia => 'ورود رسانه'; + + @override + String get edit => 'ویرایش'; + + @override + String get apply => 'اعمال'; + + @override + String get hex => 'Hex'; + + @override + String get material => 'مواد'; + + @override + String get color => 'رنگ'; + + @override + String get findText => 'جستجوی متن'; + + @override + String get moveToPreviousOccurrence => 'انتقال به رخداد قبلی'; + + @override + String get moveToNextOccurrence => 'انتقال به رخداد بعدی'; + + @override + String get savedUsingTheNetwork => 'با استفاده از شبکه ذخیره شده است'; + + @override + String get savedUsingLocalStorage => + 'ذخیره شده با استفاده از فضای ذخیره محلی'; + + @override + String get errorWhileSavingImage => 'خطا در هنگام ذخیره تصویر'; + + @override + String get pleaseEnterTextForYourLink => + "لطفاً متن لینک خود را وارد کنید (مثال: 'بیشتر بدانید')"; + + @override + String get pleaseEnterTheLinkURL => + "لطفاً URL لینک را وارد کنید (مثال: 'https://example.com')"; + + @override + String get pleaseEnterAValidImageURL => 'لطفاً یک URL تصویر معتبر وارد کنید'; + + @override + String get pleaseEnterAValidVideoURL => 'لطفاً یک URL ویدیوی معتبر وارد کنید'; + + @override + String get photo => 'عکس'; + + @override + String get image => 'تصویر'; + + @override + String get caseSensitivityAndWholeWordSearch => + 'حساسیت به کوچکی و بزرگی حروف و جستجوی کلمه کامل'; + + @override + String get insertImage => 'وارد کردن تصویر'; +} diff --git a/lib/src/l10n/generated/quill_localizations_fr.dart b/lib/src/l10n/generated/quill_localizations_fr.dart new file mode 100644 index 00000000..7bd59ee4 --- /dev/null +++ b/lib/src/l10n/generated/quill_localizations_fr.dart @@ -0,0 +1,233 @@ +import 'quill_localizations.dart'; + +/// The translations for French (`fr`). +class FlutterQuillLocalizationsFr extends FlutterQuillLocalizations { + FlutterQuillLocalizationsFr([super.locale = 'fr']); + + @override + String get pasteLink => 'Coller un lien'; + + @override + String get ok => 'Ok'; + + @override + String get selectColor => 'Choisir une couleur'; + + @override + String get gallery => 'Galerie'; + + @override + String get link => 'Lien'; + + @override + String get open => 'Ouvrir'; + + @override + String get copy => 'Copier'; + + @override + String get remove => 'Supprimer'; + + @override + String get save => 'Sauvegarder'; + + @override + String get zoom => 'Zoomer'; + + @override + String get saved => 'Enregistrée'; + + @override + String get text => 'Texte'; + + @override + String get resize => 'Redimensionner'; + + @override + String get width => 'Largeur'; + + @override + String get height => 'Hauteur'; + + @override + String get size => 'Taille'; + + @override + String get small => 'Petit'; + + @override + String get large => 'Grand'; + + @override + String get huge => 'Énorme'; + + @override + String get clear => 'Supprimer la mise en forme'; + + @override + String get font => 'Police'; + + @override + String get search => 'Rechercher'; + + @override + String get camera => 'Caméra'; + + @override + String get video => 'Vidéo'; + + @override + String get undo => 'Annuler'; + + @override + String get redo => 'Refaire'; + + @override + String get fontFamily => 'Famille de police'; + + @override + String get fontSize => 'Taille de police'; + + @override + String get bold => 'Gras'; + + @override + String get subscript => 'Indice'; + + @override + String get superscript => 'Exposant'; + + @override + String get italic => 'Italique'; + + @override + String get underline => 'Souligné'; + + @override + String get strikeThrough => 'Barré'; + + @override + String get inlineCode => 'Code en ligne'; + + @override + String get fontColor => 'Couleur de police'; + + @override + String get backgroundColor => 'Couleur de fond'; + + @override + String get clearFormat => 'Effacer la mise en forme'; + + @override + String get alignLeft => 'Aligner à gauche'; + + @override + String get alignCenter => 'Aligner au centre'; + + @override + String get alignRight => 'Aligner à droite'; + + @override + String get justifyWinWidth => 'Justifier'; + + @override + String get textDirection => 'Direction du texte'; + + @override + String get headerStyle => "Style d'en-tête"; + + @override + String get numberedList => 'Liste numérotée'; + + @override + String get bulletList => 'Liste à puces'; + + @override + String get checkedList => 'Check-list'; + + @override + String get codeBlock => 'Bloc de code'; + + @override + String get quote => 'Citation'; + + @override + String get increaseIndent => 'Augmenter le retrait'; + + @override + String get decreaseIndent => 'Diminuer le retrait'; + + @override + String get insertURL => 'Insérer une URL'; + + @override + String get visitLink => 'Visiter'; + + @override + String get enterLink => 'Entrer un lien'; + + @override + String get enterMedia => 'Entrer un média'; + + @override + String get edit => 'Modifier'; + + @override + String get apply => 'Appliquer'; + + @override + String get hex => 'Hex'; + + @override + String get material => 'Matériel'; + + @override + String get color => 'Couleur'; + + @override + String get findText => 'Rechercher du texte'; + + @override + String get moveToPreviousOccurrence => "Aller à l'occurrence précédente"; + + @override + String get moveToNextOccurrence => "Aller à l'occurrence suivante"; + + @override + String get savedUsingTheNetwork => 'Enregistré via le réseau'; + + @override + String get savedUsingLocalStorage => + 'Enregistré en utilisant le stockage local'; + + @override + String get errorWhileSavingImage => + "Erreur lors de l'enregistrement de l'image"; + + @override + String get pleaseEnterTextForYourLink => "par exemple, 'En savoir plus'"; + + @override + String get pleaseEnterTheLinkURL => "par exemple, 'https://example.com'"; + + @override + String get pleaseEnterAValidImageURL => + "Veuillez saisir une URL d'image valide"; + + @override + String get pleaseEnterAValidVideoURL => + 'Veuillez entrer une URL vidéo valide'; + + @override + String get photo => 'Photo'; + + @override + String get image => 'Image'; + + @override + String get caseSensitivityAndWholeWordSearch => + 'Sensibilité à la casse et recherche de mots entiers'; + + @override + String get insertImage => 'Insérer une image'; +} diff --git a/lib/src/l10n/generated/quill_localizations_he.dart b/lib/src/l10n/generated/quill_localizations_he.dart new file mode 100644 index 00000000..a0d94ce0 --- /dev/null +++ b/lib/src/l10n/generated/quill_localizations_he.dart @@ -0,0 +1,231 @@ +import 'quill_localizations.dart'; + +/// The translations for Hebrew (`he`). +class FlutterQuillLocalizationsHe extends FlutterQuillLocalizations { + FlutterQuillLocalizationsHe([super.locale = 'he']); + + @override + String get pasteLink => 'הדבק את הלינק'; + + @override + String get ok => 'אוקי'; + + @override + String get selectColor => 'בחר צבע'; + + @override + String get gallery => 'גלריה'; + + @override + String get link => 'לינק'; + + @override + String get open => 'פתח'; + + @override + String get copy => 'העתק'; + + @override + String get remove => 'מחק'; + + @override + String get save => 'שמור'; + + @override + String get zoom => 'זום'; + + @override + String get saved => 'נשמר'; + + @override + String get text => 'טקסט'; + + @override + String get resize => 'שנה גודל'; + + @override + String get width => 'רוחב'; + + @override + String get height => 'גובה'; + + @override + String get size => 'גודל'; + + @override + String get small => 'קטן'; + + @override + String get large => 'גדול'; + + @override + String get huge => 'ענק'; + + @override + String get clear => 'מחוק'; + + @override + String get font => 'פונט'; + + @override + String get search => 'חפש'; + + @override + String get camera => 'מצלמה'; + + @override + String get video => 'וידאו'; + + @override + String get undo => 'בטל'; + + @override + String get redo => 'בצע שוב'; + + @override + String get fontFamily => 'משפחת הפונטים'; + + @override + String get fontSize => 'גודל הפונט'; + + @override + String get bold => 'מודגש'; + + @override + String get subscript => 'כתוב בתחתית השורה'; + + @override + String get superscript => 'כתוב בחלק העליון של השורה'; + + @override + String get italic => 'נטוי'; + + @override + String get underline => 'קו תחתון'; + + @override + String get strikeThrough => 'קו חוצה'; + + @override + String get inlineCode => 'קוד טקסט בתוך הטקסט'; + + @override + String get fontColor => 'צבע טקסט'; + + @override + String get backgroundColor => 'צבע רקע'; + + @override + String get clearFormat => 'נקה פורמט'; + + @override + String get alignLeft => 'יישור לשמאל'; + + @override + String get alignCenter => 'יישור למרכז'; + + @override + String get alignRight => 'יישור לימין'; + + @override + String get justifyWinWidth => 'יישור לרוחב החלון'; + + @override + String get textDirection => 'כיוון הטקסט'; + + @override + String get headerStyle => 'סגנון הכותרת'; + + @override + String get numberedList => 'רשימה ממוספרת'; + + @override + String get bulletList => 'רשימה עם תבליטים'; + + @override + String get checkedList => 'רשימת תיקולים'; + + @override + String get codeBlock => 'בלוק קוד'; + + @override + String get quote => 'ציטוט'; + + @override + String get increaseIndent => 'הגדל את הזחות'; + + @override + String get decreaseIndent => 'הקטן את הזחות'; + + @override + String get insertURL => 'הוסף URL'; + + @override + String get visitLink => 'בקר בלינק'; + + @override + String get enterLink => 'הכנס לינק'; + + @override + String get enterMedia => 'הכנס מדיה'; + + @override + String get edit => 'ערוך'; + + @override + String get apply => 'החל'; + + @override + String get hex => 'Hex'; + + @override + String get material => 'חומר'; + + @override + String get color => 'צבע'; + + @override + String get findText => 'מצא טקסט'; + + @override + String get moveToPreviousOccurrence => 'התקדם להופעה הקודמת'; + + @override + String get moveToNextOccurrence => 'התקדם להופעה הבאה'; + + @override + String get savedUsingTheNetwork => 'נשמר באמצעות הרשת'; + + @override + String get savedUsingLocalStorage => 'נשמר באמצעות אחסון מקומי'; + + @override + String get errorWhileSavingImage => 'שגיאה בעת שמירת התמונה'; + + @override + String get pleaseEnterTextForYourLink => + "אנא הזן טקסט לקישור שלך (לדוגמה, 'מידע נוסף')"; + + @override + String get pleaseEnterTheLinkURL => + "אנא הזן את כתובת ה-URL של הקישור (לדוגמה, 'https://example.com')"; + + @override + String get pleaseEnterAValidImageURL => 'אנא הזן כתובת URL תקינה של תמונה'; + + @override + String get pleaseEnterAValidVideoURL => 'אנא הזן כתובת URL תקינה של וידיאו'; + + @override + String get photo => 'תמונה'; + + @override + String get image => 'תמונה'; + + @override + String get caseSensitivityAndWholeWordSearch => + 'רגישות לאותות רישיות וחיפוש לפי מילה שלמה'; + + @override + String get insertImage => 'הכנס תמונה'; +} diff --git a/lib/src/l10n/generated/quill_localizations_hi.dart b/lib/src/l10n/generated/quill_localizations_hi.dart new file mode 100644 index 00000000..4e2d2d5e --- /dev/null +++ b/lib/src/l10n/generated/quill_localizations_hi.dart @@ -0,0 +1,232 @@ +import 'quill_localizations.dart'; + +/// The translations for Hindi (`hi`). +class FlutterQuillLocalizationsHi extends FlutterQuillLocalizations { + FlutterQuillLocalizationsHi([super.locale = 'hi']); + + @override + String get pasteLink => 'लिंक पेस्ट करें'; + + @override + String get ok => 'ठीक है'; + + @override + String get selectColor => 'रंग चुनें'; + + @override + String get gallery => 'गैलरी'; + + @override + String get link => 'लिंक'; + + @override + String get open => 'खोलें'; + + @override + String get copy => 'कॉपी करें'; + + @override + String get remove => 'हटाएं'; + + @override + String get save => 'सुरक्षित करें'; + + @override + String get zoom => 'बड़ा करें'; + + @override + String get saved => 'सुरक्षित कर दिया गया है'; + + @override + String get text => 'शब्द'; + + @override + String get resize => 'आकार बदलें'; + + @override + String get width => 'चौड़ाई'; + + @override + String get height => 'ऊंचाई'; + + @override + String get size => 'Size'; + + @override + String get small => 'Small'; + + @override + String get large => 'Large'; + + @override + String get huge => 'Huge'; + + @override + String get clear => 'Clear'; + + @override + String get font => 'Font'; + + @override + String get search => 'Search'; + + @override + String get camera => 'Camera'; + + @override + String get video => 'Video'; + + @override + String get undo => 'Undo'; + + @override + String get redo => 'Redo'; + + @override + String get fontFamily => 'Sूची का नाम'; + + @override + String get fontSize => 'फ़ॉन्ट का आकार'; + + @override + String get bold => 'ताक़तवर'; + + @override + String get subscript => 'अधोलेख'; + + @override + String get superscript => 'अद्भुतलेख'; + + @override + String get italic => 'तिरछा'; + + @override + String get underline => 'रेखांकन'; + + @override + String get strikeThrough => 'मार'; + + @override + String get inlineCode => 'लाइन कोड'; + + @override + String get fontColor => 'फॉन्ट का रंग'; + + @override + String get backgroundColor => 'पृष्ठभूमि का रंग'; + + @override + String get clearFormat => 'स्वच्छ स्वरूप'; + + @override + String get alignLeft => 'बाएं संरेखित करें'; + + @override + String get alignCenter => 'केंद्रित संरेखित करें'; + + @override + String get alignRight => 'दाएं संरेखित करें'; + + @override + String get justifyWinWidth => 'जस्टीफ़ी विन चौड़ाई'; + + @override + String get textDirection => 'टेक्स्ट की दिशा'; + + @override + String get headerStyle => 'हेडर शैली'; + + @override + String get numberedList => 'संख्याबद्ध सूची'; + + @override + String get bulletList => 'गोली दी गई सूची'; + + @override + String get checkedList => 'जाँची गई सूची'; + + @override + String get codeBlock => 'कोड ब्लॉक'; + + @override + String get quote => 'नोट'; + + @override + String get increaseIndent => 'इंडेंट बढ़ाएं'; + + @override + String get decreaseIndent => 'इंडेंट कम करें'; + + @override + String get insertURL => 'URL डालें'; + + @override + String get visitLink => 'लिंक देखें'; + + @override + String get enterLink => 'लिंक दर्ज करें'; + + @override + String get enterMedia => 'मीडिया दर्ज करें'; + + @override + String get edit => 'संपादित करें'; + + @override + String get apply => 'लागू करें'; + + @override + String get hex => 'हेक्स'; + + @override + String get material => 'सामग्री'; + + @override + String get color => 'रंग'; + + @override + String get findText => 'मद को खोजें'; + + @override + String get moveToPreviousOccurrence => 'पिछले घटनांतर पर जाएं'; + + @override + String get moveToNextOccurrence => 'आगामी घटनांतर पर जाएं'; + + @override + String get savedUsingTheNetwork => 'नेटवर्क का उपयोग करके सहेजा गया'; + + @override + String get savedUsingLocalStorage => + 'स्थानीय संग्रहण का उपयोग करके सहेजा गया'; + + @override + String get errorWhileSavingImage => 'तस्वीर सहेजते समय त्रुटि'; + + @override + String get pleaseEnterTextForYourLink => + "कृपया अपने लिंक के लिए एक पाठ दर्ज करें (उदाहरण: 'और अधिक जानें')"; + + @override + String get pleaseEnterTheLinkURL => + "कृपया लिंक URL दर्ज करें (उदाहरण: 'https://example.com')"; + + @override + String get pleaseEnterAValidImageURL => 'कृपया एक वैध चित्र URL दर्ज करें'; + + @override + String get pleaseEnterAValidVideoURL => 'कृपया एक वैध वीडियो URL दर्ज करें'; + + @override + String get photo => 'फोटो'; + + @override + String get image => 'छवि'; + + @override + String get caseSensitivityAndWholeWordSearch => + 'केस सेंसिटिविटी और पूरे शब्द की खोज'; + + @override + String get insertImage => 'छवि डालें'; +} diff --git a/lib/src/l10n/generated/quill_localizations_id.dart b/lib/src/l10n/generated/quill_localizations_id.dart new file mode 100644 index 00000000..26eb61cb --- /dev/null +++ b/lib/src/l10n/generated/quill_localizations_id.dart @@ -0,0 +1,233 @@ +import 'quill_localizations.dart'; + +/// The translations for Indonesian (`id`). +class FlutterQuillLocalizationsId extends FlutterQuillLocalizations { + FlutterQuillLocalizationsId([super.locale = 'id']); + + @override + String get pasteLink => 'Tempel tautan'; + + @override + String get ok => 'Oke'; + + @override + String get selectColor => 'Pilih Warna'; + + @override + String get gallery => 'Galeri'; + + @override + String get link => 'Tautan'; + + @override + String get open => 'Buka'; + + @override + String get copy => 'Salin'; + + @override + String get remove => 'Hapus'; + + @override + String get save => 'Simpan'; + + @override + String get zoom => 'Perbesar'; + + @override + String get saved => 'Tersimpan'; + + @override + String get text => 'Teks'; + + @override + String get resize => 'Ubah Ukuran'; + + @override + String get width => 'Lebar'; + + @override + String get height => 'Tinggi'; + + @override + String get size => 'Ukuran'; + + @override + String get small => 'Kecil'; + + @override + String get large => 'Besar'; + + @override + String get huge => 'Sangat Besar'; + + @override + String get clear => 'Hapus'; + + @override + String get font => 'Font'; + + @override + String get search => 'Cari'; + + @override + String get camera => 'Kamera'; + + @override + String get video => 'Video'; + + @override + String get undo => 'Undo'; + + @override + String get redo => 'Redo'; + + @override + String get fontFamily => 'Keluarga Font'; + + @override + String get fontSize => 'Ukuran Font'; + + @override + String get bold => 'Tebal'; + + @override + String get subscript => 'Subskrip'; + + @override + String get superscript => 'Superskrip'; + + @override + String get italic => 'Miring'; + + @override + String get underline => 'Garis Bawah'; + + @override + String get strikeThrough => 'Coret Saja'; + + @override + String get inlineCode => 'Kode Inline'; + + @override + String get fontColor => 'Warna Font'; + + @override + String get backgroundColor => 'Warna Latar'; + + @override + String get clearFormat => 'Hapus Format'; + + @override + String get alignLeft => 'Rata Kiri'; + + @override + String get alignCenter => 'Rata Tengah'; + + @override + String get alignRight => 'Rata Kanan'; + + @override + String get justifyWinWidth => 'Rata Kanan dan Kiri'; + + @override + String get textDirection => 'Arah Teks'; + + @override + String get headerStyle => 'Gaya Header'; + + @override + String get numberedList => 'Daftar Bernomor'; + + @override + String get bulletList => 'Daftar Poin'; + + @override + String get checkedList => 'Daftar Dicentang'; + + @override + String get codeBlock => 'Blok Kode'; + + @override + String get quote => 'Kutipan'; + + @override + String get increaseIndent => 'Tambah Indentasi'; + + @override + String get decreaseIndent => 'Kurangi Indentasi'; + + @override + String get insertURL => 'Masukkan URL'; + + @override + String get visitLink => 'Kunjungi Tautan'; + + @override + String get enterLink => 'Masukkan Tautan'; + + @override + String get enterMedia => 'Masukkan Media'; + + @override + String get edit => 'Edit'; + + @override + String get apply => 'Terapkan'; + + @override + String get hex => 'Hex'; + + @override + String get material => 'Material'; + + @override + String get color => 'Warna'; + + @override + String get findText => 'Temukan Teks'; + + @override + String get moveToPreviousOccurrence => 'Pindah ke Kejadian Sebelumnya'; + + @override + String get moveToNextOccurrence => 'Pindah ke Kejadian Berikutnya'; + + @override + String get savedUsingTheNetwork => 'Tersimpan menggunakan jaringan'; + + @override + String get savedUsingLocalStorage => + 'Tersimpan menggunakan penyimpanan lokal'; + + @override + String get errorWhileSavingImage => 'Error saat menyimpan gambar'; + + @override + String get pleaseEnterTextForYourLink => + "Harap masukkan teks untuk tautan Anda (contoh: 'Pelajari lebih lanjut')"; + + @override + String get pleaseEnterTheLinkURL => + "Harap masukkan URL tautan (contoh: 'https://example.com')"; + + @override + String get pleaseEnterAValidImageURL => + 'Harap masukkan URL gambar yang valid'; + + @override + String get pleaseEnterAValidVideoURL => 'Harap masukkan URL video yang valid'; + + @override + String get photo => 'Foto'; + + @override + String get image => 'Gambar'; + + @override + String get caseSensitivityAndWholeWordSearch => + 'Sensitivitas huruf besar dan kecil dan pencarian kata utuh'; + + @override + String get insertImage => 'Sisipkan Gambar'; +} diff --git a/lib/src/l10n/generated/quill_localizations_it.dart b/lib/src/l10n/generated/quill_localizations_it.dart new file mode 100644 index 00000000..9d6c18ee --- /dev/null +++ b/lib/src/l10n/generated/quill_localizations_it.dart @@ -0,0 +1,233 @@ +import 'quill_localizations.dart'; + +/// The translations for Italian (`it`). +class FlutterQuillLocalizationsIt extends FlutterQuillLocalizations { + FlutterQuillLocalizationsIt([super.locale = 'it']); + + @override + String get pasteLink => 'Incolla un collegamento'; + + @override + String get ok => 'Ok'; + + @override + String get selectColor => 'Seleziona Colore'; + + @override + String get gallery => 'Galleria'; + + @override + String get link => 'Collegamento'; + + @override + String get open => 'Apri'; + + @override + String get copy => 'Copia'; + + @override + String get remove => 'Rimuovi'; + + @override + String get save => 'Salva'; + + @override + String get zoom => 'Ingrandisci'; + + @override + String get saved => 'Salvato'; + + @override + String get text => 'Testo'; + + @override + String get resize => 'Ridimensiona'; + + @override + String get width => 'Larghezza'; + + @override + String get height => 'Altezza'; + + @override + String get size => 'Dimensione'; + + @override + String get small => 'Piccolo'; + + @override + String get large => 'Largo'; + + @override + String get huge => 'Enorme'; + + @override + String get clear => 'Cancella'; + + @override + String get font => 'Font'; + + @override + String get search => 'Ricerca'; + + @override + String get camera => 'Camera'; + + @override + String get video => 'Video'; + + @override + String get undo => 'Annulla'; + + @override + String get redo => 'Ripeti'; + + @override + String get fontFamily => 'Famiglia del carattere'; + + @override + String get fontSize => 'Dimensione del carattere'; + + @override + String get bold => 'Grassetto'; + + @override + String get subscript => 'Pedice'; + + @override + String get superscript => 'Apice'; + + @override + String get italic => 'Corsivo'; + + @override + String get underline => 'Sottolineato'; + + @override + String get strikeThrough => 'Barrato'; + + @override + String get inlineCode => 'Codice inline'; + + @override + String get fontColor => 'Colore del carattere'; + + @override + String get backgroundColor => 'Colore di sfondo'; + + @override + String get clearFormat => 'Cancella formato'; + + @override + String get alignLeft => 'Allinea a sinistra'; + + @override + String get alignCenter => 'Allinea al centro'; + + @override + String get alignRight => 'Allinea a destra'; + + @override + String get justifyWinWidth => 'Giustifica per larghezza finestra'; + + @override + String get textDirection => 'Direzione testo'; + + @override + String get headerStyle => 'Stile intestazione'; + + @override + String get numberedList => 'Elenco numerato'; + + @override + String get bulletList => 'Elenco puntato'; + + @override + String get checkedList => 'Elenco con segni di spunta'; + + @override + String get codeBlock => 'Blocco di codice'; + + @override + String get quote => 'Citazione'; + + @override + String get increaseIndent => 'Aumenta rientro'; + + @override + String get decreaseIndent => 'Diminuisci rientro'; + + @override + String get insertURL => 'Inserisci URL'; + + @override + String get visitLink => 'Visita il collegamento'; + + @override + String get enterLink => 'Inserisci il collegamento'; + + @override + String get enterMedia => 'Inserisci multimedia'; + + @override + String get edit => 'Modifica'; + + @override + String get apply => 'Applica'; + + @override + String get hex => 'Esadecimale'; + + @override + String get material => 'Materiale'; + + @override + String get color => 'Colore'; + + @override + String get findText => 'Trova testo'; + + @override + String get moveToPreviousOccurrence => "Vai all'occorrenza precedente"; + + @override + String get moveToNextOccurrence => "Vai all'occorrenza successiva"; + + @override + String get savedUsingTheNetwork => 'Salvato utilizzando la rete'; + + @override + String get savedUsingLocalStorage => + 'Salvato utilizzando la memorizzazione locale'; + + @override + String get errorWhileSavingImage => + "Errore durante il salvataggio dell'immagine"; + + @override + String get pleaseEnterTextForYourLink => + "Inserisci un testo per il tuo link (ad esempio, 'Per saperne di più')"; + + @override + String get pleaseEnterTheLinkURL => + "Inserisci l'URL del link (ad esempio, 'https://example.com')"; + + @override + String get pleaseEnterAValidImageURL => 'Inserisci un URL di immagine valido'; + + @override + String get pleaseEnterAValidVideoURL => 'Inserisci un URL video valido'; + + @override + String get photo => 'Foto'; + + @override + String get image => 'Immagine'; + + @override + String get caseSensitivityAndWholeWordSearch => + 'Sensibilità maiuscole/minuscole e ricerca di parole intere'; + + @override + String get insertImage => 'Inserisci immagine'; +} diff --git a/lib/src/l10n/generated/quill_localizations_ja.dart b/lib/src/l10n/generated/quill_localizations_ja.dart new file mode 100644 index 00000000..94927b83 --- /dev/null +++ b/lib/src/l10n/generated/quill_localizations_ja.dart @@ -0,0 +1,228 @@ +import 'quill_localizations.dart'; + +/// The translations for Japanese (`ja`). +class FlutterQuillLocalizationsJa extends FlutterQuillLocalizations { + FlutterQuillLocalizationsJa([super.locale = 'ja']); + + @override + String get pasteLink => 'リンクをペースト'; + + @override + String get ok => '完了'; + + @override + String get selectColor => '色を選択'; + + @override + String get gallery => '写真集'; + + @override + String get link => 'リンク'; + + @override + String get open => '開く'; + + @override + String get copy => 'コピー'; + + @override + String get remove => '削除'; + + @override + String get save => '保存'; + + @override + String get zoom => '拡大'; + + @override + String get saved => '保存済み'; + + @override + String get text => '文字'; + + @override + String get resize => 'サイズを調整'; + + @override + String get width => '幅'; + + @override + String get height => '高さ'; + + @override + String get size => 'サイズ'; + + @override + String get small => '小さい'; + + @override + String get large => '大きい'; + + @override + String get huge => 'でっかい'; + + @override + String get clear => 'クリア'; + + @override + String get font => 'フォント'; + + @override + String get search => '検索'; + + @override + String get camera => 'カメラ'; + + @override + String get video => 'ビデオ'; + + @override + String get undo => '取り消し'; + + @override + String get redo => 'やり直し'; + + @override + String get fontFamily => 'フォントファミリー'; + + @override + String get fontSize => 'フォントサイズ'; + + @override + String get bold => '太字'; + + @override + String get subscript => '下付き'; + + @override + String get superscript => '上付き'; + + @override + String get italic => '斜体'; + + @override + String get underline => '下線'; + + @override + String get strikeThrough => '取り消し線'; + + @override + String get inlineCode => 'インラインコード'; + + @override + String get fontColor => 'フォントカラー'; + + @override + String get backgroundColor => 'ベースカラー'; + + @override + String get clearFormat => 'クリアフォーマット'; + + @override + String get alignLeft => '左揃え'; + + @override + String get alignCenter => 'センターアライメント'; + + @override + String get alignRight => '右揃え'; + + @override + String get justifyWinWidth => '両端揃え'; + + @override + String get textDirection => '文字方向'; + + @override + String get headerStyle => 'タイトルスタイル'; + + @override + String get numberedList => '順序付きリスト'; + + @override + String get bulletList => '順序無しリスト'; + + @override + String get checkedList => 'チェックボックス'; + + @override + String get codeBlock => 'コード'; + + @override + String get quote => '引用'; + + @override + String get increaseIndent => 'インデントを増やす'; + + @override + String get decreaseIndent => 'インデントを減らす'; + + @override + String get insertURL => 'ハイパーリンクを挿入'; + + @override + String get visitLink => 'ハイパーリンクを訪問'; + + @override + String get enterLink => 'ハイパーリンクを輸入'; + + @override + String get enterMedia => 'ミディアムを輸入'; + + @override + String get edit => '編集'; + + @override + String get apply => '応用'; + + @override + String get hex => 'Hex'; + + @override + String get material => 'Material'; + + @override + String get color => 'Color'; + + @override + String get findText => '検索テキスト'; + + @override + String get moveToPreviousOccurrence => '前のマッチ'; + + @override + String get moveToNextOccurrence => '次のマッチ'; + + @override + String get savedUsingTheNetwork => 'ネットワークを使用して保存'; + + @override + String get savedUsingLocalStorage => 'ローカルストレージを使用して保存'; + + @override + String get errorWhileSavingImage => '画像の保存中にエラーが発生しました'; + + @override + String get pleaseEnterTextForYourLink => "例: 'Learn more'"; + + @override + String get pleaseEnterTheLinkURL => "例: 'https://example.com'"; + + @override + String get pleaseEnterAValidImageURL => '有効な画像URLを入力してください'; + + @override + String get pleaseEnterAValidVideoURL => '有効なビデオURLを入力してください'; + + @override + String get photo => '写真'; + + @override + String get image => '画像'; + + @override + String get caseSensitivityAndWholeWordSearch => '大文字と小文字の区別と完全一致検索'; + + @override + String get insertImage => '画像を挿入'; +} diff --git a/lib/src/l10n/generated/quill_localizations_ko.dart b/lib/src/l10n/generated/quill_localizations_ko.dart new file mode 100644 index 00000000..bb6d87de --- /dev/null +++ b/lib/src/l10n/generated/quill_localizations_ko.dart @@ -0,0 +1,228 @@ +import 'quill_localizations.dart'; + +/// The translations for Korean (`ko`). +class FlutterQuillLocalizationsKo extends FlutterQuillLocalizations { + FlutterQuillLocalizationsKo([super.locale = 'ko']); + + @override + String get pasteLink => '링크를 붙여넣어 주세요.'; + + @override + String get ok => '확인'; + + @override + String get selectColor => '색상 선택'; + + @override + String get gallery => '갤러리'; + + @override + String get link => '링크'; + + @override + String get open => '열기'; + + @override + String get copy => '복사하기'; + + @override + String get remove => '제거하기'; + + @override + String get save => '저장하기'; + + @override + String get zoom => '확대하기'; + + @override + String get saved => '저장되었습니다.'; + + @override + String get text => '텍스트'; + + @override + String get resize => '크기조정'; + + @override + String get width => '넓이'; + + @override + String get height => '높이'; + + @override + String get size => '크기'; + + @override + String get small => '작게'; + + @override + String get large => '크게'; + + @override + String get huge => '매우크게'; + + @override + String get clear => '초기화'; + + @override + String get font => '글꼴'; + + @override + String get search => '검색'; + + @override + String get camera => '카메라'; + + @override + String get video => '비디오'; + + @override + String get undo => 'Undo'; + + @override + String get redo => 'Redo'; + + @override + String get fontFamily => 'Font family'; + + @override + String get fontSize => 'Font size'; + + @override + String get bold => 'Bold'; + + @override + String get subscript => 'Subscript'; + + @override + String get superscript => 'Superscript'; + + @override + String get italic => 'Italic'; + + @override + String get underline => 'Underline'; + + @override + String get strikeThrough => 'Strike through'; + + @override + String get inlineCode => 'Inline code'; + + @override + String get fontColor => 'Font color'; + + @override + String get backgroundColor => 'Background color'; + + @override + String get clearFormat => 'Clear format'; + + @override + String get alignLeft => 'Align left'; + + @override + String get alignCenter => 'Align center'; + + @override + String get alignRight => 'Align right'; + + @override + String get justifyWinWidth => 'Justify win width'; + + @override + String get textDirection => 'Text direction'; + + @override + String get headerStyle => 'Header style'; + + @override + String get numberedList => 'Numbered list'; + + @override + String get bulletList => 'Bullet list'; + + @override + String get checkedList => 'Checked list'; + + @override + String get codeBlock => 'Code block'; + + @override + String get quote => 'Quote'; + + @override + String get increaseIndent => 'Increase indent'; + + @override + String get decreaseIndent => 'Decrease indent'; + + @override + String get insertURL => 'Insert URL'; + + @override + String get visitLink => 'Visit link'; + + @override + String get enterLink => 'Enter link'; + + @override + String get enterMedia => 'Enter media'; + + @override + String get edit => 'Edit'; + + @override + String get apply => 'Apply'; + + @override + String get hex => 'Hex'; + + @override + String get material => 'Material'; + + @override + String get color => 'Color'; + + @override + String get findText => 'Find text'; + + @override + String get moveToPreviousOccurrence => 'Move to previous occurrence'; + + @override + String get moveToNextOccurrence => 'Move to next occurrence'; + + @override + String get savedUsingTheNetwork => 'Saved using the network'; + + @override + String get savedUsingLocalStorage => 'Saved using the local storage'; + + @override + String get errorWhileSavingImage => 'Error while saving image'; + + @override + String get pleaseEnterTextForYourLink => "e.g., 'Learn more'"; + + @override + String get pleaseEnterTheLinkURL => "e.g., 'https://example.com'"; + + @override + String get pleaseEnterAValidImageURL => 'Please enter a valid image URL'; + + @override + String get pleaseEnterAValidVideoURL => '유효한 비디오 URL을 입력하세요'; + + @override + String get photo => '사진'; + + @override + String get image => '이미지'; + + @override + String get caseSensitivityAndWholeWordSearch => '대소문자 구분 및 전체 단어 검색'; + + @override + String get insertImage => '이미지 삽입'; +} diff --git a/lib/src/l10n/generated/quill_localizations_ms.dart b/lib/src/l10n/generated/quill_localizations_ms.dart new file mode 100644 index 00000000..b46f2fd9 --- /dev/null +++ b/lib/src/l10n/generated/quill_localizations_ms.dart @@ -0,0 +1,231 @@ +import 'quill_localizations.dart'; + +/// The translations for Malay (`ms`). +class FlutterQuillLocalizationsMs extends FlutterQuillLocalizations { + FlutterQuillLocalizationsMs([super.locale = 'ms']); + + @override + String get pasteLink => 'Tampal Pautan'; + + @override + String get ok => 'Ok'; + + @override + String get selectColor => 'Pilih Warna'; + + @override + String get gallery => 'Galeri'; + + @override + String get link => 'Pautan'; + + @override + String get open => 'Buka'; + + @override + String get copy => 'Salin'; + + @override + String get remove => 'Buang'; + + @override + String get save => 'Simpan'; + + @override + String get zoom => 'Zum'; + + @override + String get saved => 'Telah Disimpan'; + + @override + String get text => 'Perkataan'; + + @override + String get resize => 'Ubah saiz'; + + @override + String get width => 'Lebar'; + + @override + String get height => 'Tinggi'; + + @override + String get size => 'Saiz'; + + @override + String get small => 'Kecil'; + + @override + String get large => 'Besar'; + + @override + String get huge => 'Amat Besar'; + + @override + String get clear => 'Padam'; + + @override + String get font => 'Fon'; + + @override + String get search => 'Carian'; + + @override + String get camera => 'Kamera'; + + @override + String get video => 'Video'; + + @override + String get undo => 'Undo'; + + @override + String get redo => 'Redo'; + + @override + String get fontFamily => 'Font family'; + + @override + String get fontSize => 'Font size'; + + @override + String get bold => 'Bold'; + + @override + String get subscript => 'Subscript'; + + @override + String get superscript => 'Superscript'; + + @override + String get italic => 'Italic'; + + @override + String get underline => 'Underline'; + + @override + String get strikeThrough => 'Strike through'; + + @override + String get inlineCode => 'Inline code'; + + @override + String get fontColor => 'Font color'; + + @override + String get backgroundColor => 'Background color'; + + @override + String get clearFormat => 'Clear format'; + + @override + String get alignLeft => 'Align left'; + + @override + String get alignCenter => 'Align center'; + + @override + String get alignRight => 'Align right'; + + @override + String get justifyWinWidth => 'Justify win width'; + + @override + String get textDirection => 'Text direction'; + + @override + String get headerStyle => 'Header style'; + + @override + String get numberedList => 'Numbered list'; + + @override + String get bulletList => 'Bullet list'; + + @override + String get checkedList => 'Checked list'; + + @override + String get codeBlock => 'Code block'; + + @override + String get quote => 'Quote'; + + @override + String get increaseIndent => 'Increase indent'; + + @override + String get decreaseIndent => 'Decrease indent'; + + @override + String get insertURL => 'Insert URL'; + + @override + String get visitLink => 'Visit link'; + + @override + String get enterLink => 'Enter link'; + + @override + String get enterMedia => 'Enter media'; + + @override + String get edit => 'Edit'; + + @override + String get apply => 'Apply'; + + @override + String get hex => 'Hex'; + + @override + String get material => 'Bahan'; + + @override + String get color => 'Warna'; + + @override + String get findText => 'Find text'; + + @override + String get moveToPreviousOccurrence => 'Move to previous occurrence'; + + @override + String get moveToNextOccurrence => 'Move to next occurrence'; + + @override + String get savedUsingTheNetwork => 'Disimpan menggunakan rangkaian'; + + @override + String get savedUsingLocalStorage => 'Saved using the local storage'; + + @override + String get errorWhileSavingImage => 'Error while saving image'; + + @override + String get pleaseEnterTextForYourLink => + "Sila masukkan teks untuk pautan anda (contoh, 'Ketahui lebih lanjut')"; + + @override + String get pleaseEnterTheLinkURL => + "Sila masukkan URL pautan (contoh, 'https://example.com')"; + + @override + String get pleaseEnterAValidImageURL => 'Sila masukkan URL imej yang sah'; + + @override + String get pleaseEnterAValidVideoURL => 'Sila masukkan URL video yang sah'; + + @override + String get photo => 'Foto'; + + @override + String get image => 'Imej'; + + @override + String get caseSensitivityAndWholeWordSearch => + 'Sensitiviti huruf besar dan kecil dan carian penuh perkataan'; + + @override + String get insertImage => 'Masukkan imej'; +} diff --git a/lib/src/l10n/generated/quill_localizations_nl.dart b/lib/src/l10n/generated/quill_localizations_nl.dart new file mode 100644 index 00000000..4b8b444c --- /dev/null +++ b/lib/src/l10n/generated/quill_localizations_nl.dart @@ -0,0 +1,233 @@ +import 'quill_localizations.dart'; + +/// The translations for Dutch Flemish (`nl`). +class FlutterQuillLocalizationsNl extends FlutterQuillLocalizations { + FlutterQuillLocalizationsNl([super.locale = 'nl']); + + @override + String get pasteLink => 'Plak een link'; + + @override + String get ok => 'Ok'; + + @override + String get selectColor => 'Selecteer kleur'; + + @override + String get gallery => 'Gallerij'; + + @override + String get link => 'Link'; + + @override + String get open => 'Open'; + + @override + String get copy => 'Kopieer'; + + @override + String get remove => 'Verwijderd'; + + @override + String get save => 'Opslaan'; + + @override + String get zoom => 'Zoom'; + + @override + String get saved => 'Opgeslagen'; + + @override + String get text => 'Tekst'; + + @override + String get resize => 'Formaat wijzigen'; + + @override + String get width => 'Breedte'; + + @override + String get height => 'Hoogte'; + + @override + String get size => 'Grootte'; + + @override + String get small => 'Small'; + + @override + String get large => 'Large'; + + @override + String get huge => 'Huge'; + + @override + String get clear => 'Clear'; + + @override + String get font => 'Font'; + + @override + String get search => 'Search'; + + @override + String get camera => 'Camera'; + + @override + String get video => 'Video'; + + @override + String get undo => 'Undo'; + + @override + String get redo => 'Redo'; + + @override + String get fontFamily => 'Font family'; + + @override + String get fontSize => 'Font size'; + + @override + String get bold => 'Bold'; + + @override + String get subscript => 'Subscript'; + + @override + String get superscript => 'Superscript'; + + @override + String get italic => 'Italic'; + + @override + String get underline => 'Underline'; + + @override + String get strikeThrough => 'Strike through'; + + @override + String get inlineCode => 'Inline code'; + + @override + String get fontColor => 'Font color'; + + @override + String get backgroundColor => 'Background color'; + + @override + String get clearFormat => 'Clear format'; + + @override + String get alignLeft => 'Align left'; + + @override + String get alignCenter => 'Align center'; + + @override + String get alignRight => 'Align right'; + + @override + String get justifyWinWidth => 'Justify win width'; + + @override + String get textDirection => 'Text direction'; + + @override + String get headerStyle => 'Header style'; + + @override + String get numberedList => 'Numbered list'; + + @override + String get bulletList => 'Bullet list'; + + @override + String get checkedList => 'Checked list'; + + @override + String get codeBlock => 'Code block'; + + @override + String get quote => 'Quote'; + + @override + String get increaseIndent => 'Increase indent'; + + @override + String get decreaseIndent => 'Decrease indent'; + + @override + String get insertURL => 'Insert URL'; + + @override + String get visitLink => 'Visit link'; + + @override + String get enterLink => 'Enter link'; + + @override + String get enterMedia => 'Enter media'; + + @override + String get edit => 'Edit'; + + @override + String get apply => 'Apply'; + + @override + String get hex => 'Hex'; + + @override + String get material => 'Materiaal'; + + @override + String get color => 'Kleur'; + + @override + String get findText => 'Find text'; + + @override + String get moveToPreviousOccurrence => 'Move to previous occurrence'; + + @override + String get moveToNextOccurrence => 'Move to next occurrence'; + + @override + String get savedUsingTheNetwork => 'Opgeslagen via het netwerk'; + + @override + String get savedUsingLocalStorage => 'Saved using the local storage'; + + @override + String get errorWhileSavingImage => 'Error while saving image'; + + @override + String get pleaseEnterTextForYourLink => + "Voer tekst in voor uw link (bijvoorbeeld 'Meer weten')"; + + @override + String get pleaseEnterTheLinkURL => + "Voer de URL van de link in (bijvoorbeeld 'https://example.com')"; + + @override + String get pleaseEnterAValidImageURL => + 'Voer een geldige URL voor de afbeelding in'; + + @override + String get pleaseEnterAValidVideoURL => + 'Voer een geldige URL voor de video in'; + + @override + String get photo => 'Foto'; + + @override + String get image => 'Afbeelding'; + + @override + String get caseSensitivityAndWholeWordSearch => + 'Hoofdlettergevoeligheid en volledig woord zoeken'; + + @override + String get insertImage => 'Afbeelding invoegen'; +} diff --git a/lib/src/l10n/generated/quill_localizations_no.dart b/lib/src/l10n/generated/quill_localizations_no.dart new file mode 100644 index 00000000..37bf4cf0 --- /dev/null +++ b/lib/src/l10n/generated/quill_localizations_no.dart @@ -0,0 +1,233 @@ +import 'quill_localizations.dart'; + +/// The translations for Norwegian (`no`). +class FlutterQuillLocalizationsNo extends FlutterQuillLocalizations { + FlutterQuillLocalizationsNo([super.locale = 'no']); + + @override + String get pasteLink => 'Lim inn lenke'; + + @override + String get ok => 'Ok'; + + @override + String get selectColor => 'Velg farge'; + + @override + String get gallery => 'Galleri'; + + @override + String get link => 'Lenke'; + + @override + String get open => 'Åpne'; + + @override + String get copy => 'Kopier'; + + @override + String get remove => 'Fjern'; + + @override + String get save => 'Lagre'; + + @override + String get zoom => 'Zoom'; + + @override + String get saved => 'Lagret'; + + @override + String get text => 'Tekst'; + + @override + String get resize => 'Endre størrelse'; + + @override + String get width => 'Bredde'; + + @override + String get height => 'Høyde'; + + @override + String get size => 'Størrelse'; + + @override + String get small => 'Liten'; + + @override + String get large => 'Stor'; + + @override + String get huge => 'Enorm'; + + @override + String get clear => 'Fjern'; + + @override + String get font => 'Skrifttype'; + + @override + String get search => 'Søk'; + + @override + String get camera => 'Kamera'; + + @override + String get video => 'Video'; + + @override + String get undo => 'Angre'; + + @override + String get redo => 'Gjør om'; + + @override + String get fontFamily => 'Skriftfamilie'; + + @override + String get fontSize => 'Skriftstørrelse'; + + @override + String get bold => 'Fet'; + + @override + String get subscript => 'Senket skrift'; + + @override + String get superscript => 'Hevet skrift'; + + @override + String get italic => 'Kursiv'; + + @override + String get underline => 'Understreket'; + + @override + String get strikeThrough => 'Gjennomstreking'; + + @override + String get inlineCode => 'In-line kode'; + + @override + String get fontColor => 'Skriftfarge'; + + @override + String get backgroundColor => 'Bakgrunnsfarge'; + + @override + String get clearFormat => 'Fjern formatering'; + + @override + String get alignLeft => 'Venstrejuster'; + + @override + String get alignCenter => 'Sentrer'; + + @override + String get alignRight => 'Høyrejuster'; + + @override + String get justifyWinWidth => 'Rettferdiggjør bredden'; + + @override + String get textDirection => 'Tekstretning'; + + @override + String get headerStyle => 'Overskriftsstil'; + + @override + String get numberedList => 'Nummerert liste'; + + @override + String get bulletList => 'Punktliste'; + + @override + String get checkedList => 'Avkrysset liste'; + + @override + String get codeBlock => 'Kodeblokk'; + + @override + String get quote => 'Sitert tekst'; + + @override + String get increaseIndent => 'Øk innrykk'; + + @override + String get decreaseIndent => 'Mink innrykk'; + + @override + String get insertURL => 'Sett inn URL'; + + @override + String get visitLink => 'Besøk lenken'; + + @override + String get enterLink => 'Skriv inn lenken'; + + @override + String get enterMedia => 'Sett inn media'; + + @override + String get edit => 'Rediger'; + + @override + String get apply => 'Bruk'; + + @override + String get hex => 'Hex'; + + @override + String get material => 'Materiale'; + + @override + String get color => 'Farge'; + + @override + String get findText => 'Finn tekst'; + + @override + String get moveToPreviousOccurrence => 'Gå til forrige forekomst'; + + @override + String get moveToNextOccurrence => 'Gå til neste forekomst'; + + @override + String get savedUsingTheNetwork => 'Lagret ved hjelp av nettverket'; + + @override + String get savedUsingLocalStorage => 'Lagret ved hjelp av lokal lagring'; + + @override + String get errorWhileSavingImage => 'Feil ved lagring av bilde'; + + @override + String get pleaseEnterTextForYourLink => + "Vennligst skriv inn tekst for lenken din (for eksempel 'Lær mer')"; + + @override + String get pleaseEnterTheLinkURL => + "Vennligst skriv inn lenkens URL (for eksempel 'https://example.com')"; + + @override + String get pleaseEnterAValidImageURL => + 'Vennligst skriv inn en gyldig bilde-URL'; + + @override + String get pleaseEnterAValidVideoURL => + 'Vennligst skriv inn en gyldig video-URL'; + + @override + String get photo => 'Bilde'; + + @override + String get image => 'Bilde'; + + @override + String get caseSensitivityAndWholeWordSearch => + 'Stor/liten bokstavfølsomhet og helordsøk'; + + @override + String get insertImage => 'Sett inn bilde'; +} diff --git a/lib/src/l10n/generated/quill_localizations_pl.dart b/lib/src/l10n/generated/quill_localizations_pl.dart new file mode 100644 index 00000000..b983356b --- /dev/null +++ b/lib/src/l10n/generated/quill_localizations_pl.dart @@ -0,0 +1,230 @@ +import 'quill_localizations.dart'; + +/// The translations for Polish (`pl`). +class FlutterQuillLocalizationsPl extends FlutterQuillLocalizations { + FlutterQuillLocalizationsPl([super.locale = 'pl']); + + @override + String get pasteLink => 'Wklej link'; + + @override + String get ok => 'OK'; + + @override + String get selectColor => 'Wybierz kolor'; + + @override + String get gallery => 'Galeria'; + + @override + String get link => 'Link'; + + @override + String get open => 'Otwórz'; + + @override + String get copy => 'Kopiuj'; + + @override + String get remove => 'Usuń'; + + @override + String get save => 'Zapisz'; + + @override + String get zoom => 'Powiększenie'; + + @override + String get saved => 'Zapisano'; + + @override + String get text => 'Tekst'; + + @override + String get resize => 'Resize'; + + @override + String get width => 'Width'; + + @override + String get height => 'Height'; + + @override + String get size => 'Size'; + + @override + String get small => 'Small'; + + @override + String get large => 'Large'; + + @override + String get huge => 'Huge'; + + @override + String get clear => 'Clear'; + + @override + String get font => 'Font'; + + @override + String get search => 'Search'; + + @override + String get camera => 'Camera'; + + @override + String get video => 'Video'; + + @override + String get undo => 'Undo'; + + @override + String get redo => 'Redo'; + + @override + String get fontFamily => 'Font family'; + + @override + String get fontSize => 'Font size'; + + @override + String get bold => 'Bold'; + + @override + String get subscript => 'Subscript'; + + @override + String get superscript => 'Superscript'; + + @override + String get italic => 'Italic'; + + @override + String get underline => 'Underline'; + + @override + String get strikeThrough => 'Strike through'; + + @override + String get inlineCode => 'Inline code'; + + @override + String get fontColor => 'Font color'; + + @override + String get backgroundColor => 'Background color'; + + @override + String get clearFormat => 'Clear format'; + + @override + String get alignLeft => 'Align left'; + + @override + String get alignCenter => 'Align center'; + + @override + String get alignRight => 'Align right'; + + @override + String get justifyWinWidth => 'Justify win width'; + + @override + String get textDirection => 'Text direction'; + + @override + String get headerStyle => 'Header style'; + + @override + String get numberedList => 'Numbered list'; + + @override + String get bulletList => 'Bullet list'; + + @override + String get checkedList => 'Checked list'; + + @override + String get codeBlock => 'Code block'; + + @override + String get quote => 'Quote'; + + @override + String get increaseIndent => 'Increase indent'; + + @override + String get decreaseIndent => 'Decrease indent'; + + @override + String get insertURL => 'Insert URL'; + + @override + String get visitLink => 'Visit link'; + + @override + String get enterLink => 'Enter link'; + + @override + String get enterMedia => 'Enter media'; + + @override + String get edit => 'Edit'; + + @override + String get apply => 'Apply'; + + @override + String get hex => 'Hex'; + + @override + String get material => 'Materiał'; + + @override + String get color => 'Kolor'; + + @override + String get findText => 'Find text'; + + @override + String get moveToPreviousOccurrence => 'Move to previous occurrence'; + + @override + String get moveToNextOccurrence => 'Move to next occurrence'; + + @override + String get savedUsingTheNetwork => 'Saved using the network'; + + @override + String get savedUsingLocalStorage => 'Saved using the local storage'; + + @override + String get errorWhileSavingImage => 'Error while saving image'; + + @override + String get pleaseEnterTextForYourLink => "e.g., 'Learn more'"; + + @override + String get pleaseEnterTheLinkURL => "e.g., 'https://example.com'"; + + @override + String get pleaseEnterAValidImageURL => 'Please enter a valid image URL'; + + @override + String get pleaseEnterAValidVideoURL => + 'Proszę wprowadzić poprawny adres URL wideo'; + + @override + String get photo => 'Zdjęcie'; + + @override + String get image => 'Obraz'; + + @override + String get caseSensitivityAndWholeWordSearch => + 'Czułość na wielkość liter i wyszukiwanie całego słowa'; + + @override + String get insertImage => 'Wstaw obraz'; +} diff --git a/lib/src/l10n/generated/quill_localizations_pt.dart b/lib/src/l10n/generated/quill_localizations_pt.dart new file mode 100644 index 00000000..a48362b1 --- /dev/null +++ b/lib/src/l10n/generated/quill_localizations_pt.dart @@ -0,0 +1,460 @@ +import 'quill_localizations.dart'; + +/// The translations for Portuguese (`pt`). +class FlutterQuillLocalizationsPt extends FlutterQuillLocalizations { + FlutterQuillLocalizationsPt([super.locale = 'pt']); + + @override + String get pasteLink => 'Colar um link'; + + @override + String get ok => 'Ok'; + + @override + String get selectColor => 'Selecionar uma cor'; + + @override + String get gallery => 'Galeria'; + + @override + String get link => 'Link'; + + @override + String get open => 'Abra'; + + @override + String get copy => 'Copiar'; + + @override + String get remove => 'Remover'; + + @override + String get save => 'Salvar'; + + @override + String get zoom => 'Zoom'; + + @override + String get saved => 'Salvo'; + + @override + String get text => 'Texto'; + + @override + String get resize => 'Redimencionar'; + + @override + String get width => 'Largura'; + + @override + String get height => 'Altura'; + + @override + String get size => 'Tamanho'; + + @override + String get small => 'Pequeno'; + + @override + String get large => 'Grande'; + + @override + String get huge => 'Gigante'; + + @override + String get clear => 'Limpar'; + + @override + String get font => 'Fonte'; + + @override + String get search => 'Search'; + + @override + String get camera => 'Camera'; + + @override + String get video => 'Video'; + + @override + String get undo => 'Undo'; + + @override + String get redo => 'Redo'; + + @override + String get fontFamily => 'Font family'; + + @override + String get fontSize => 'Font size'; + + @override + String get bold => 'Bold'; + + @override + String get subscript => 'Subscript'; + + @override + String get superscript => 'Superscript'; + + @override + String get italic => 'Italic'; + + @override + String get underline => 'Underline'; + + @override + String get strikeThrough => 'Strike through'; + + @override + String get inlineCode => 'Inline code'; + + @override + String get fontColor => 'Font color'; + + @override + String get backgroundColor => 'Background color'; + + @override + String get clearFormat => 'Clear format'; + + @override + String get alignLeft => 'Align left'; + + @override + String get alignCenter => 'Align center'; + + @override + String get alignRight => 'Align right'; + + @override + String get justifyWinWidth => 'Justify win width'; + + @override + String get textDirection => 'Text direction'; + + @override + String get headerStyle => 'Header style'; + + @override + String get numberedList => 'Numbered list'; + + @override + String get bulletList => 'Bullet list'; + + @override + String get checkedList => 'Checked list'; + + @override + String get codeBlock => 'Code block'; + + @override + String get quote => 'Quote'; + + @override + String get increaseIndent => 'Increase indent'; + + @override + String get decreaseIndent => 'Decrease indent'; + + @override + String get insertURL => 'Insert URL'; + + @override + String get visitLink => 'Visit link'; + + @override + String get enterLink => 'Enter link'; + + @override + String get enterMedia => 'Enter media'; + + @override + String get edit => 'Edit'; + + @override + String get apply => 'Apply'; + + @override + String get hex => 'Hex'; + + @override + String get material => 'Material'; + + @override + String get color => 'Cor'; + + @override + String get findText => 'Find text'; + + @override + String get moveToPreviousOccurrence => 'Move to previous occurrence'; + + @override + String get moveToNextOccurrence => 'Move to next occurrence'; + + @override + String get savedUsingTheNetwork => 'Guardado através da network'; + + @override + String get savedUsingLocalStorage => + 'Guardado através do armazenamento local'; + + @override + String get errorWhileSavingImage => 'Erro a gravar imagem'; + + @override + String get pleaseEnterTextForYourLink => "e.g., 'Learn more'"; + + @override + String get pleaseEnterTheLinkURL => "e.g., 'https://example.com'"; + + @override + String get pleaseEnterAValidImageURL => 'Please enter a valid image URL'; + + @override + String get pleaseEnterAValidVideoURL => + 'Por favor, insira uma URL de vídeo válida'; + + @override + String get photo => 'Foto'; + + @override + String get image => 'Imagem'; + + @override + String get caseSensitivityAndWholeWordSearch => + 'Sensibilidade a maiúsculas e minúsculas e pesquisa de palavras inteiras'; + + @override + String get insertImage => 'Inserir imagem'; +} + +/// The translations for Portuguese, as used in Brazil (`pt_BR`). +class FlutterQuillLocalizationsPtBr extends FlutterQuillLocalizationsPt { + FlutterQuillLocalizationsPtBr() : super('pt_BR'); + + @override + String get pasteLink => 'Colar um link'; + + @override + String get ok => 'Ok'; + + @override + String get selectColor => 'Selecionar uma cor'; + + @override + String get gallery => 'Galeria'; + + @override + String get link => 'Link'; + + @override + String get open => 'Abrir'; + + @override + String get copy => 'Copiar'; + + @override + String get remove => 'Remover'; + + @override + String get save => 'Salvar'; + + @override + String get zoom => 'Zoom'; + + @override + String get saved => 'Salvo'; + + @override + String get text => 'Texto'; + + @override + String get resize => 'Redimensionar'; + + @override + String get width => 'Largura'; + + @override + String get height => 'Altura'; + + @override + String get size => 'Tamanho'; + + @override + String get small => 'Pequeno'; + + @override + String get large => 'Grande'; + + @override + String get huge => 'Gigante'; + + @override + String get clear => 'Limpar'; + + @override + String get font => 'Fonte'; + + @override + String get search => 'Buscar'; + + @override + String get camera => 'Câmera'; + + @override + String get video => 'Vídeo'; + + @override + String get undo => 'Desfazer'; + + @override + String get redo => 'Refazer'; + + @override + String get fontFamily => 'Fonte'; + + @override + String get fontSize => 'Tamanho da fonte'; + + @override + String get bold => 'Negrito'; + + @override + String get subscript => 'Subscrito'; + + @override + String get superscript => 'Sobrescrito'; + + @override + String get italic => 'Itálico'; + + @override + String get underline => 'Sublinhado'; + + @override + String get strikeThrough => 'Tachado'; + + @override + String get inlineCode => 'Inline code'; + + @override + String get fontColor => 'Cor da fonte'; + + @override + String get backgroundColor => 'Cor do fundo'; + + @override + String get clearFormat => 'Limpar formatação'; + + @override + String get alignLeft => 'Texto à esquerda'; + + @override + String get alignCenter => 'Centralizar'; + + @override + String get alignRight => 'Texto à direita'; + + @override + String get justifyWinWidth => 'Justificado'; + + @override + String get textDirection => 'Direção do texto'; + + @override + String get headerStyle => 'Estilo de cabeçalho'; + + @override + String get numberedList => 'Numeração'; + + @override + String get bulletList => 'Marcadores'; + + @override + String get checkedList => 'Lista de verificação'; + + @override + String get codeBlock => 'Code block'; + + @override + String get quote => 'Citação'; + + @override + String get increaseIndent => 'Aumentar recuo'; + + @override + String get decreaseIndent => 'Diminuir recuo'; + + @override + String get insertURL => 'Inserir URL'; + + @override + String get visitLink => 'Visitar link'; + + @override + String get enterLink => 'Inserir link'; + + @override + String get enterMedia => 'Inserir mídia'; + + @override + String get edit => 'Editar'; + + @override + String get apply => 'Aplicar'; + + @override + String get hex => 'Hex'; + + @override + String get material => 'Material'; + + @override + String get color => 'Cor'; + + @override + String get findText => 'Find text'; + + @override + String get moveToPreviousOccurrence => 'Move to previous occurrence'; + + @override + String get moveToNextOccurrence => 'Move to next occurrence'; + + @override + String get savedUsingTheNetwork => 'Saved using the network'; + + @override + String get savedUsingLocalStorage => 'Saved using the local storage'; + + @override + String get errorWhileSavingImage => 'Error while saving image'; + + @override + String get pleaseEnterTextForYourLink => "e.g., 'Learn more'"; + + @override + String get pleaseEnterTheLinkURL => "e.g., 'https://example.com'"; + + @override + String get pleaseEnterAValidImageURL => 'Please enter a valid image URL'; + + @override + String get pleaseEnterAValidVideoURL => + 'Por favor, insira uma URL de vídeo válida'; + + @override + String get photo => 'Foto'; + + @override + String get image => 'Imagem'; + + @override + String get caseSensitivityAndWholeWordSearch => + 'Sensibilidade a maiúsculas e minúsculas e pesquisa de palavras inteiras'; + + @override + String get insertImage => 'Inserir imagem'; +} diff --git a/lib/src/l10n/generated/quill_localizations_ru.dart b/lib/src/l10n/generated/quill_localizations_ru.dart new file mode 100644 index 00000000..376e3c12 --- /dev/null +++ b/lib/src/l10n/generated/quill_localizations_ru.dart @@ -0,0 +1,230 @@ +import 'quill_localizations.dart'; + +/// The translations for Russian (`ru`). +class FlutterQuillLocalizationsRu extends FlutterQuillLocalizations { + FlutterQuillLocalizationsRu([super.locale = 'ru']); + + @override + String get pasteLink => 'Вставить ссылку'; + + @override + String get ok => 'ОК'; + + @override + String get selectColor => 'Выбрать цвет'; + + @override + String get gallery => 'Галерея'; + + @override + String get link => 'Ссылка'; + + @override + String get open => 'Открыть'; + + @override + String get copy => 'Копировать'; + + @override + String get remove => 'Удалить'; + + @override + String get save => 'Сохранить'; + + @override + String get zoom => 'Увеличить'; + + @override + String get saved => 'Сохранено'; + + @override + String get text => 'Текст'; + + @override + String get resize => 'Resize'; + + @override + String get width => 'Width'; + + @override + String get height => 'Height'; + + @override + String get size => 'Size'; + + @override + String get small => 'Small'; + + @override + String get large => 'Large'; + + @override + String get huge => 'Huge'; + + @override + String get clear => 'Clear'; + + @override + String get font => 'Font'; + + @override + String get search => 'Search'; + + @override + String get camera => 'Camera'; + + @override + String get video => 'Video'; + + @override + String get undo => 'Undo'; + + @override + String get redo => 'Redo'; + + @override + String get fontFamily => 'Font family'; + + @override + String get fontSize => 'Font size'; + + @override + String get bold => 'Bold'; + + @override + String get subscript => 'Subscript'; + + @override + String get superscript => 'Superscript'; + + @override + String get italic => 'Italic'; + + @override + String get underline => 'Underline'; + + @override + String get strikeThrough => 'Strike through'; + + @override + String get inlineCode => 'Inline code'; + + @override + String get fontColor => 'Font color'; + + @override + String get backgroundColor => 'Background color'; + + @override + String get clearFormat => 'Clear format'; + + @override + String get alignLeft => 'Align left'; + + @override + String get alignCenter => 'Align center'; + + @override + String get alignRight => 'Align right'; + + @override + String get justifyWinWidth => 'Justify win width'; + + @override + String get textDirection => 'Text direction'; + + @override + String get headerStyle => 'Header style'; + + @override + String get numberedList => 'Numbered list'; + + @override + String get bulletList => 'Bullet list'; + + @override + String get checkedList => 'Checked list'; + + @override + String get codeBlock => 'Code block'; + + @override + String get quote => 'Quote'; + + @override + String get increaseIndent => 'Increase indent'; + + @override + String get decreaseIndent => 'Decrease indent'; + + @override + String get insertURL => 'Insert URL'; + + @override + String get visitLink => 'Visit link'; + + @override + String get enterLink => 'Enter link'; + + @override + String get enterMedia => 'Enter media'; + + @override + String get edit => 'Edit'; + + @override + String get apply => 'Apply'; + + @override + String get hex => 'Hex'; + + @override + String get material => 'Материал'; + + @override + String get color => 'Цвет'; + + @override + String get findText => 'Find text'; + + @override + String get moveToPreviousOccurrence => 'Move to previous occurrence'; + + @override + String get moveToNextOccurrence => 'Move to next occurrence'; + + @override + String get savedUsingTheNetwork => 'Saved using the network'; + + @override + String get savedUsingLocalStorage => 'Saved using the local storage'; + + @override + String get errorWhileSavingImage => 'Error while saving image'; + + @override + String get pleaseEnterTextForYourLink => "e.g., 'Learn more'"; + + @override + String get pleaseEnterTheLinkURL => "e.g., 'https://example.com'"; + + @override + String get pleaseEnterAValidImageURL => 'Please enter a valid image URL'; + + @override + String get pleaseEnterAValidVideoURL => + 'Пожалуйста, введите действительный URL-адрес видео'; + + @override + String get photo => 'Фото'; + + @override + String get image => 'Изображение'; + + @override + String get caseSensitivityAndWholeWordSearch => + 'Чувствительность к регистру и поиск целых слов'; + + @override + String get insertImage => 'Вставить изображение'; +} diff --git a/lib/src/l10n/generated/quill_localizations_sr.dart b/lib/src/l10n/generated/quill_localizations_sr.dart new file mode 100644 index 00000000..e49dcdb7 --- /dev/null +++ b/lib/src/l10n/generated/quill_localizations_sr.dart @@ -0,0 +1,232 @@ +import 'quill_localizations.dart'; + +/// The translations for Serbian (`sr`). +class FlutterQuillLocalizationsSr extends FlutterQuillLocalizations { + FlutterQuillLocalizationsSr([super.locale = 'sr']); + + @override + String get pasteLink => 'Nalepi vezu'; + + @override + String get ok => 'OK'; + + @override + String get selectColor => 'Odaberi boju'; + + @override + String get gallery => 'Galerija'; + + @override + String get link => 'Veza'; + + @override + String get open => 'Otvori'; + + @override + String get copy => 'Kopiraj'; + + @override + String get remove => 'Ukloni'; + + @override + String get save => 'Sačuvaj'; + + @override + String get zoom => 'Uvećaj'; + + @override + String get saved => 'Sačuvano'; + + @override + String get text => 'Tekst'; + + @override + String get resize => 'Promeni veličinu'; + + @override + String get width => 'Širina'; + + @override + String get height => 'Visina'; + + @override + String get size => 'Veličina'; + + @override + String get small => 'Malo'; + + @override + String get large => 'Veliko'; + + @override + String get huge => 'Ogromno'; + + @override + String get clear => 'Obriši'; + + @override + String get font => 'Font'; + + @override + String get search => 'Pretraga'; + + @override + String get camera => 'Kamera'; + + @override + String get video => 'Video'; + + @override + String get undo => 'Poništi'; + + @override + String get redo => 'Ponovo'; + + @override + String get fontFamily => 'Porodica fonta'; + + @override + String get fontSize => 'Veličina fonta'; + + @override + String get bold => 'Podebljano'; + + @override + String get subscript => 'Indeks'; + + @override + String get superscript => 'Stepen'; + + @override + String get italic => 'Iskošeno'; + + @override + String get underline => 'Podvučeno'; + + @override + String get strikeThrough => 'Precrtano'; + + @override + String get inlineCode => 'Ugrađeni kôd'; + + @override + String get fontColor => 'Boja fonta'; + + @override + String get backgroundColor => 'Boja pozadine'; + + @override + String get clearFormat => 'Obriši format'; + + @override + String get alignLeft => 'Poravnanje levo'; + + @override + String get alignCenter => 'Poravnanje centar'; + + @override + String get alignRight => 'Poravnanje desno'; + + @override + String get justifyWinWidth => 'Centriraj širinu prozora'; + + @override + String get textDirection => 'Smer teksta'; + + @override + String get headerStyle => 'Stil zaglavlja'; + + @override + String get numberedList => 'Numerisana lista'; + + @override + String get bulletList => 'Lista sa znakovima'; + + @override + String get checkedList => 'Proverena lista'; + + @override + String get codeBlock => 'Blok koda'; + + @override + String get quote => 'Citat'; + + @override + String get increaseIndent => 'Povećaj uvlačenje'; + + @override + String get decreaseIndent => 'Smanji uvlačenje'; + + @override + String get insertURL => 'Ubaci URL'; + + @override + String get visitLink => 'Poseti link'; + + @override + String get enterLink => 'Unesi link'; + + @override + String get enterMedia => 'Unesi medij'; + + @override + String get edit => 'Uredi'; + + @override + String get apply => 'Primeni'; + + @override + String get hex => 'Hex'; + + @override + String get material => 'Materijal'; + + @override + String get color => 'Boja'; + + @override + String get findText => 'Nađi tekst'; + + @override + String get moveToPreviousOccurrence => 'Idi na prethodno pojavljivanje'; + + @override + String get moveToNextOccurrence => 'Idi na sledeće pojavljivanje'; + + @override + String get savedUsingTheNetwork => 'Sačuvano korišćenjem mreže'; + + @override + String get savedUsingLocalStorage => + 'Sačuvano korišćenjem lokalnog skladišta'; + + @override + String get errorWhileSavingImage => 'Greška pri čuvanju slike'; + + @override + String get pleaseEnterTextForYourLink => + "Unesite tekst za svoj link (na primer, 'Saznajte više')"; + + @override + String get pleaseEnterTheLinkURL => + "Unesite URL linka (na primer, 'https://example.com')"; + + @override + String get pleaseEnterAValidImageURL => 'Unesite važeći URL slike'; + + @override + String get pleaseEnterAValidVideoURL => 'Unesite važeći URL videa'; + + @override + String get photo => 'Foto'; + + @override + String get image => 'Slika'; + + @override + String get caseSensitivityAndWholeWordSearch => + 'Osetljivost na velika i mala slova i potraga za celom rečju'; + + @override + String get insertImage => 'Umetni sliku'; +} diff --git a/lib/src/l10n/generated/quill_localizations_sw.dart b/lib/src/l10n/generated/quill_localizations_sw.dart new file mode 100644 index 00000000..5928e04e --- /dev/null +++ b/lib/src/l10n/generated/quill_localizations_sw.dart @@ -0,0 +1,230 @@ +import 'quill_localizations.dart'; + +/// The translations for Swahili (`sw`). +class FlutterQuillLocalizationsSw extends FlutterQuillLocalizations { + FlutterQuillLocalizationsSw([super.locale = 'sw']); + + @override + String get pasteLink => 'Bandika Kiungo'; + + @override + String get ok => 'Sawa'; + + @override + String get selectColor => 'Chagua Rangi'; + + @override + String get gallery => 'Matunzio'; + + @override + String get link => 'Kiungo'; + + @override + String get open => 'Fungua'; + + @override + String get copy => 'Nakili'; + + @override + String get remove => 'Ondoa'; + + @override + String get save => 'Hifadhi'; + + @override + String get zoom => 'Kuza'; + + @override + String get saved => 'Imehifadhiwa'; + + @override + String get text => 'Maandishi'; + + @override + String get resize => 'Badilisha Ukubwa'; + + @override + String get width => 'Upana'; + + @override + String get height => 'Urefu'; + + @override + String get size => 'Ukubwa'; + + @override + String get small => 'Ndogo'; + + @override + String get large => 'Kubwa'; + + @override + String get huge => 'Kubwa Sana'; + + @override + String get clear => 'Wazi'; + + @override + String get font => 'Fonti'; + + @override + String get search => 'Tafuta'; + + @override + String get camera => 'Kamera'; + + @override + String get video => 'Video'; + + @override + String get undo => 'Fanyua'; + + @override + String get redo => 'Fanya Upya'; + + @override + String get fontFamily => 'Familia ya Fonti'; + + @override + String get fontSize => 'Ukubwa wa Fonti'; + + @override + String get bold => 'Nono'; + + @override + String get subscript => 'Maandishi ys Chini'; + + @override + String get superscript => 'Maandishi ya Juu'; + + @override + String get italic => 'Italiki'; + + @override + String get underline => 'Pigia Mstari'; + + @override + String get strikeThrough => 'Ghairi Maandishi'; + + @override + String get inlineCode => 'Codi ya Laini Moja'; + + @override + String get fontColor => 'Rangi ya Fonti'; + + @override + String get backgroundColor => 'Rangi ya Nyuma'; + + @override + String get clearFormat => 'Muundo Wazi'; + + @override + String get alignLeft => 'Pangilia Kushoto'; + + @override + String get alignCenter => 'Pangilia Kati'; + + @override + String get alignRight => 'Pangilia Kulia'; + + @override + String get justifyWinWidth => 'Kuhalalisha Upana wa Ushindi'; + + @override + String get textDirection => 'Mwelekeo wa Maandishi'; + + @override + String get headerStyle => 'Mtindo wa Mada'; + + @override + String get numberedList => 'Orodha ya Nambari'; + + @override + String get bulletList => 'Orodha ya Risasi'; + + @override + String get checkedList => 'Orodha iliyoangaliwa'; + + @override + String get codeBlock => 'aya ya codi'; + + @override + String get quote => 'Nukuu'; + + @override + String get increaseIndent => 'Ongeza Ujongezaji'; + + @override + String get decreaseIndent => 'Punguza Ujongezaji'; + + @override + String get insertURL => 'Ingiza Kiungo'; + + @override + String get visitLink => 'Tembelea Kiungo'; + + @override + String get enterLink => 'Ingiza Kiungo'; + + @override + String get enterMedia => 'Ingiza Picha'; + + @override + String get edit => 'Harir'; + + @override + String get apply => 'Weka'; + + @override + String get hex => 'Hexi'; + + @override + String get material => 'Nyenzo'; + + @override + String get color => 'Rangi'; + + @override + String get findText => 'Pata Maandishi'; + + @override + String get moveToPreviousOccurrence => 'Nenda Kwenye Tukio la Awali'; + + @override + String get moveToNextOccurrence => 'Nenda kwa Tukio linalofuata'; + + @override + String get savedUsingTheNetwork => 'Imehifadhiwa kwa kutumia mtandao'; + + @override + String get savedUsingLocalStorage => 'Imehifadhiwa kwa Hifadhi ya Ndani'; + + @override + String get errorWhileSavingImage => 'Hitilafu Wakati wa Kuhifadhi Picha'; + + @override + String get pleaseEnterTextForYourLink => "Kwa mfano, 'Jifunze zaidi'"; + + @override + String get pleaseEnterTheLinkURL => "Kwa mfano, 'https://example.com'"; + + @override + String get pleaseEnterAValidImageURL => + 'Tafadhali ingiza URL halali ya picha'; + + @override + String get pleaseEnterAValidVideoURL => 'Tafadhali ingiza URL ya video ili'; + + @override + String get photo => 'Picha'; + + @override + String get image => 'Picha'; + + @override + String get caseSensitivityAndWholeWordSearch => + 'Uwiano wa herufi kubwa na ndogo na utafutaji wa neno zima'; + + @override + String get insertImage => 'Weka Picha'; +} diff --git a/lib/src/l10n/generated/quill_localizations_tk.dart b/lib/src/l10n/generated/quill_localizations_tk.dart new file mode 100644 index 00000000..56631328 --- /dev/null +++ b/lib/src/l10n/generated/quill_localizations_tk.dart @@ -0,0 +1,229 @@ +import 'quill_localizations.dart'; + +/// The translations for Turkmen (`tk`). +class FlutterQuillLocalizationsTk extends FlutterQuillLocalizations { + FlutterQuillLocalizationsTk([super.locale = 'tk']); + + @override + String get pasteLink => 'Baglanyşygy goýuň'; + + @override + String get ok => 'Bolýar'; + + @override + String get selectColor => 'Reňk saýlaň'; + + @override + String get gallery => 'Galereýa'; + + @override + String get link => 'Baglanyşyk'; + + @override + String get open => 'Aç'; + + @override + String get copy => 'Kopýala'; + + @override + String get remove => 'Poz'; + + @override + String get save => 'Sakla'; + + @override + String get zoom => 'Ulalt'; + + @override + String get saved => 'Saklandy'; + + @override + String get text => 'Tekst'; + + @override + String get resize => 'Ölçegini üýtget'; + + @override + String get width => 'In'; + + @override + String get height => 'Boý'; + + @override + String get size => 'Ölçegi'; + + @override + String get small => 'Kiçi'; + + @override + String get large => 'Uly'; + + @override + String get huge => 'Has uly'; + + @override + String get clear => 'Arassala'; + + @override + String get font => 'Şrift'; + + @override + String get search => 'Gözleg'; + + @override + String get camera => 'Kamera'; + + @override + String get video => 'Wideo'; + + @override + String get undo => 'Yza al'; + + @override + String get redo => 'Öňe al'; + + @override + String get fontFamily => 'Şrift maşgalasy'; + + @override + String get fontSize => 'Şrift ululygy'; + + @override + String get bold => 'Galyň'; + + @override + String get subscript => 'Aşaky ýazgy'; + + @override + String get superscript => 'Ýokarky ýazgy'; + + @override + String get italic => 'Italik'; + + @override + String get underline => 'Aşagyny çyz'; + + @override + String get strikeThrough => 'Üstüni çyz'; + + @override + String get inlineCode => 'Bir setirde kod'; + + @override + String get fontColor => 'Şrift reňki'; + + @override + String get backgroundColor => 'Arka reňki'; + + @override + String get clearFormat => 'Formaty arassala'; + + @override + String get alignLeft => 'Çepe deňleşdir'; + + @override + String get alignCenter => 'Orta deňleşdir'; + + @override + String get alignRight => 'Saga deňleşdir'; + + @override + String get justifyWinWidth => 'Justify win width'; + + @override + String get textDirection => 'Tekst ugry'; + + @override + String get headerStyle => 'Sözbaşy stili'; + + @override + String get numberedList => 'Sanly sanaw'; + + @override + String get bulletList => 'Okly sanawy'; + + @override + String get checkedList => 'Tikli sanaw'; + + @override + String get codeBlock => 'Kod blogy'; + + @override + String get quote => 'Sitata'; + + @override + String get increaseIndent => 'Indent köpelt'; + + @override + String get decreaseIndent => 'Indent azalt'; + + @override + String get insertURL => 'URL goý'; + + @override + String get visitLink => 'Baglanyşyga giriň'; + + @override + String get enterLink => 'Baglanyşyk giriň'; + + @override + String get enterMedia => 'Mediýa giriziň'; + + @override + String get edit => 'Üýtget'; + + @override + String get apply => 'Ulan'; + + @override + String get hex => 'Hex'; + + @override + String get material => 'Material'; + + @override + String get color => 'Reňk'; + + @override + String get findText => 'Tekst tapyň'; + + @override + String get moveToPreviousOccurrence => 'Öňki hadysa geçiň'; + + @override + String get moveToNextOccurrence => 'Indiki hadysa geçiň'; + + @override + String get savedUsingTheNetwork => 'Ulgama ulanyp saklanan'; + + @override + String get savedUsingLocalStorage => 'Saved using the local storage'; + + @override + String get errorWhileSavingImage => 'Error while saving image'; + + @override + String get pleaseEnterTextForYourLink => 'Güýz öwrenmek)'; + + @override + String get pleaseEnterTheLinkURL => 'https://example.com'; + + @override + String get pleaseEnterAValidImageURL => 'Please enter a valid image URL'; + + @override + String get pleaseEnterAValidVideoURL => 'Lütfen güýjük wideo URL giriziň'; + + @override + String get photo => 'Surat'; + + @override + String get image => 'Surat'; + + @override + String get caseSensitivityAndWholeWordSearch => + 'Iňkisar we iň oňg söz gözleýinç'; + + @override + String get insertImage => 'Surat goş'; +} diff --git a/lib/src/l10n/generated/quill_localizations_tr.dart b/lib/src/l10n/generated/quill_localizations_tr.dart new file mode 100644 index 00000000..dbd5d3de --- /dev/null +++ b/lib/src/l10n/generated/quill_localizations_tr.dart @@ -0,0 +1,230 @@ +import 'quill_localizations.dart'; + +/// The translations for Turkish (`tr`). +class FlutterQuillLocalizationsTr extends FlutterQuillLocalizations { + FlutterQuillLocalizationsTr([super.locale = 'tr']); + + @override + String get pasteLink => 'Bağlantıyı Yapıştır'; + + @override + String get ok => 'Tamam'; + + @override + String get selectColor => 'Renk Seçin'; + + @override + String get gallery => 'Galeri'; + + @override + String get link => 'Bağlantı'; + + @override + String get open => 'Açık'; + + @override + String get copy => 'Kopyala'; + + @override + String get remove => 'Kaldır'; + + @override + String get save => 'Kayıt Et'; + + @override + String get zoom => 'Yakınlaştır'; + + @override + String get saved => 'Kaydedildi'; + + @override + String get text => 'Text'; + + @override + String get resize => 'Yeniden Boyutlandır'; + + @override + String get width => 'Genişlik'; + + @override + String get height => 'Yükseklik'; + + @override + String get size => 'Boyut'; + + @override + String get small => 'Küçük'; + + @override + String get large => 'Büyük'; + + @override + String get huge => 'Daha Büyük'; + + @override + String get clear => 'Temizle'; + + @override + String get font => 'Yazı tipi'; + + @override + String get search => 'Ara'; + + @override + String get camera => 'Kamera'; + + @override + String get video => 'Video'; + + @override + String get undo => 'Geri'; + + @override + String get redo => 'İleri'; + + @override + String get fontFamily => 'Yazı Türü'; + + @override + String get fontSize => 'Yazı Boyutu'; + + @override + String get bold => 'Kalın'; + + @override + String get subscript => 'Alt Simge'; + + @override + String get superscript => 'Üst Simge'; + + @override + String get italic => 'İtalik'; + + @override + String get underline => 'Altı Çizili'; + + @override + String get strikeThrough => 'Üsti Çizili'; + + @override + String get inlineCode => 'Inline code'; + + @override + String get fontColor => 'Yazı Rengi'; + + @override + String get backgroundColor => 'Vurgu Rengi'; + + @override + String get clearFormat => 'Formatı Temizle'; + + @override + String get alignLeft => 'Sola Hizala'; + + @override + String get alignCenter => 'Ortaya Hizala'; + + @override + String get alignRight => 'Sağa Hizala'; + + @override + String get justifyWinWidth => 'Kenarlara Hizala'; + + @override + String get textDirection => 'Metin Yönü'; + + @override + String get headerStyle => 'Başlık Stili'; + + @override + String get numberedList => 'Numaralı Liste'; + + @override + String get bulletList => 'Madde Listesi'; + + @override + String get checkedList => 'Kontrol Listesi'; + + @override + String get codeBlock => 'Kod Blogu'; + + @override + String get quote => 'Alıntı'; + + @override + String get increaseIndent => 'Girintiyi Artır'; + + @override + String get decreaseIndent => 'Girintiyi Azalt'; + + @override + String get insertURL => 'URL Giriniz'; + + @override + String get visitLink => 'Bağlantıyı Ziyaret Et'; + + @override + String get enterLink => 'Bağlantı Giriniz'; + + @override + String get enterMedia => 'Medya Giriniz'; + + @override + String get edit => 'Düzenle'; + + @override + String get apply => 'Uygula'; + + @override + String get hex => 'Hex'; + + @override + String get material => 'Malzeme'; + + @override + String get color => 'Renk'; + + @override + String get findText => 'Find text'; + + @override + String get moveToPreviousOccurrence => 'Move to previous occurrence'; + + @override + String get moveToNextOccurrence => 'Move to next occurrence'; + + @override + String get savedUsingTheNetwork => 'Saved using the network'; + + @override + String get savedUsingLocalStorage => 'Saved using the local storage'; + + @override + String get errorWhileSavingImage => 'Error while saving image'; + + @override + String get pleaseEnterTextForYourLink => "e.g., 'Learn more'"; + + @override + String get pleaseEnterTheLinkURL => "e.g., 'https://example.com'"; + + @override + String get pleaseEnterAValidImageURL => 'Please enter a valid image URL'; + + @override + String get pleaseEnterAValidVideoURL => + "Lütfen geçerli bir video URL'si girin"; + + @override + String get photo => 'Fotoğraf'; + + @override + String get image => 'Görüntü'; + + @override + String get caseSensitivityAndWholeWordSearch => + 'Büyük/küçük harf hassasiyeti ve tam kelime arama'; + + @override + String get insertImage => 'Görüntü ekle'; +} diff --git a/lib/src/l10n/generated/quill_localizations_uk.dart b/lib/src/l10n/generated/quill_localizations_uk.dart new file mode 100644 index 00000000..163c299f --- /dev/null +++ b/lib/src/l10n/generated/quill_localizations_uk.dart @@ -0,0 +1,232 @@ +import 'quill_localizations.dart'; + +/// The translations for Ukrainian (`uk`). +class FlutterQuillLocalizationsUk extends FlutterQuillLocalizations { + FlutterQuillLocalizationsUk([super.locale = 'uk']); + + @override + String get pasteLink => 'Вставити посилання'; + + @override + String get ok => 'ОК'; + + @override + String get selectColor => 'Вибрати колір'; + + @override + String get gallery => 'Галерея'; + + @override + String get link => 'Посилання'; + + @override + String get open => 'Відкрити'; + + @override + String get copy => 'Копіювати'; + + @override + String get remove => 'Видалити'; + + @override + String get save => 'Зберегти'; + + @override + String get zoom => 'Збільшити'; + + @override + String get saved => 'Збережено'; + + @override + String get text => 'Текст'; + + @override + String get resize => 'Змінити розмір'; + + @override + String get width => 'Ширина'; + + @override + String get height => 'Висота'; + + @override + String get size => 'Розмір'; + + @override + String get small => 'Малий'; + + @override + String get large => 'Великий'; + + @override + String get huge => 'Величезний'; + + @override + String get clear => 'Очистити'; + + @override + String get font => 'Шрифт'; + + @override + String get search => 'Пошук'; + + @override + String get camera => 'Камера'; + + @override + String get video => 'Відео'; + + @override + String get undo => 'Скасувати'; + + @override + String get redo => 'Повторити'; + + @override + String get fontFamily => 'Сімейство шрифтів'; + + @override + String get fontSize => 'Розмір шрифту'; + + @override + String get bold => 'Жирний'; + + @override + String get subscript => 'Нижній індекс'; + + @override + String get superscript => 'Верхній індекс'; + + @override + String get italic => 'Курсив'; + + @override + String get underline => 'Підкреслити'; + + @override + String get strikeThrough => 'Закреслений'; + + @override + String get inlineCode => 'Вбудований код'; + + @override + String get fontColor => 'Колір шрифту'; + + @override + String get backgroundColor => 'Колір фону'; + + @override + String get clearFormat => 'Очистити формат'; + + @override + String get alignLeft => 'Вирівняти ліворуч'; + + @override + String get alignCenter => 'Вирівняти по центру'; + + @override + String get alignRight => 'Вирівняти праворуч'; + + @override + String get justifyWinWidth => 'Вирівняти за шириною вікна'; + + @override + String get textDirection => 'Напрямок тексту'; + + @override + String get headerStyle => 'Стиль заголовка'; + + @override + String get numberedList => 'Нумерований список'; + + @override + String get bulletList => 'Маркований список'; + + @override + String get checkedList => 'Список з позначками'; + + @override + String get codeBlock => 'Блок коду'; + + @override + String get quote => 'Цитата'; + + @override + String get increaseIndent => 'Збільшити відступ'; + + @override + String get decreaseIndent => 'Зменшити відступ'; + + @override + String get insertURL => 'Вставити URL'; + + @override + String get visitLink => 'Відвідати посилання'; + + @override + String get enterLink => 'Ввести посилання'; + + @override + String get enterMedia => 'Ввести медіа'; + + @override + String get edit => 'Редагувати'; + + @override + String get apply => 'Застосувати'; + + @override + String get hex => 'Hex'; + + @override + String get material => 'Матеріал'; + + @override + String get color => 'Колір'; + + @override + String get findText => 'Знайти текст'; + + @override + String get moveToPreviousOccurrence => 'Перейти до попереднього випадку'; + + @override + String get moveToNextOccurrence => 'Перейти до наступного випадку'; + + @override + String get savedUsingTheNetwork => 'Збережено за допомогою мережі'; + + @override + String get savedUsingLocalStorage => + 'Збережено за допомогою локального сховища'; + + @override + String get errorWhileSavingImage => 'Помилка при збереженні зображення'; + + @override + String get pleaseEnterTextForYourLink => "Наприклад, 'Дізнатися більше'"; + + @override + String get pleaseEnterTheLinkURL => "Наприклад, 'https://example.com'"; + + @override + String get pleaseEnterAValidImageURL => + 'Будь ласка, введіть правильний URL-адресу зображення'; + + @override + String get pleaseEnterAValidVideoURL => + 'Будь ласка, введіть дійсну URL-адресу відео'; + + @override + String get photo => 'Фото'; + + @override + String get image => 'Зображення'; + + @override + String get caseSensitivityAndWholeWordSearch => + 'Чутливість до регістру та пошук цілих слів'; + + @override + String get insertImage => 'Вставити зображення'; +} diff --git a/lib/src/l10n/generated/quill_localizations_ur.dart b/lib/src/l10n/generated/quill_localizations_ur.dart new file mode 100644 index 00000000..2f4b14fd --- /dev/null +++ b/lib/src/l10n/generated/quill_localizations_ur.dart @@ -0,0 +1,234 @@ +import 'quill_localizations.dart'; + +/// The translations for Urdu (`ur`). +class FlutterQuillLocalizationsUr extends FlutterQuillLocalizations { + FlutterQuillLocalizationsUr([super.locale = 'ur']); + + @override + String get pasteLink => 'لنک پیسٹ کریں'; + + @override + String get ok => 'ٹھیک ہے'; + + @override + String get selectColor => 'رنگ منتخب کریں'; + + @override + String get gallery => 'گیلری'; + + @override + String get link => 'لنک'; + + @override + String get open => 'کھولیں'; + + @override + String get copy => 'نقل'; + + @override + String get remove => 'ہٹا دیں'; + + @override + String get save => 'محفوظ کریں'; + + @override + String get zoom => 'زوم'; + + @override + String get saved => 'محفوظ کر لیا'; + + @override + String get text => 'متن'; + + @override + String get resize => 'سائز تبدیل کریں۔'; + + @override + String get width => 'چوڑائی'; + + @override + String get height => 'اونچائی'; + + @override + String get size => 'سائز'; + + @override + String get small => 'چھوٹا'; + + @override + String get large => 'بڑا'; + + @override + String get huge => 'بہت بڑا'; + + @override + String get clear => 'صاف'; + + @override + String get font => 'فونٹ'; + + @override + String get search => 'تلاش'; + + @override + String get camera => 'کیمرا'; + + @override + String get video => 'ویڈیو'; + + @override + String get undo => 'واپس'; + + @override + String get redo => 'دوبارہ'; + + @override + String get fontFamily => 'فونٹ خاندان'; + + @override + String get fontSize => 'فونٹ سائز'; + + @override + String get bold => 'ڈہوکی'; + + @override + String get subscript => 'نیچے لکھا'; + + @override + String get superscript => 'اوپر لکھا'; + + @override + String get italic => 'ٹیک کیا'; + + @override + String get underline => 'نیچے خط'; + + @override + String get strikeThrough => 'خط خوراک'; + + @override + String get inlineCode => 'ان لائن کوڈ'; + + @override + String get fontColor => 'فونٹ کا رنگ'; + + @override + String get backgroundColor => 'پس منظر کا رنگ'; + + @override + String get clearFormat => 'فارمیٹ صاف کریں'; + + @override + String get alignLeft => 'بائیں ہم آہنگ ہوں'; + + @override + String get alignCenter => 'مرکز میں ہم آہنگ ہوں'; + + @override + String get alignRight => 'دائیں ہم آہنگ ہوں'; + + @override + String get justifyWinWidth => 'جسٹیفائی ون چوڑائی'; + + @override + String get textDirection => 'متن کی سمت'; + + @override + String get headerStyle => 'ہیڈر کا انداز'; + + @override + String get numberedList => 'مرقم فہرست'; + + @override + String get bulletList => 'گولی فہرست'; + + @override + String get checkedList => 'چیک کی گئی فہرست'; + + @override + String get codeBlock => 'کوڈ بلاک'; + + @override + String get quote => 'حوالہ'; + + @override + String get increaseIndent => 'درجہ بڑھائیں'; + + @override + String get decreaseIndent => 'درجہ گھٹائیں'; + + @override + String get insertURL => 'یو آر ایل درج کریں'; + + @override + String get visitLink => 'لنک دیکھیں'; + + @override + String get enterLink => 'لنک درج کریں'; + + @override + String get enterMedia => 'میڈیا درج کریں'; + + @override + String get edit => 'ترتیب دیں'; + + @override + String get apply => 'لگائیں'; + + @override + String get hex => 'ہیکس'; + + @override + String get material => 'مواد'; + + @override + String get color => 'رنگ'; + + @override + String get findText => 'متن تلاش کریں'; + + @override + String get moveToPreviousOccurrence => 'پچھلے واقعہ پر منتقل ہوں'; + + @override + String get moveToNextOccurrence => 'اگلے واقعہ پر منتقل ہوں'; + + @override + String get savedUsingTheNetwork => 'نیٹ ورک کا استعمال کر کے محفوظ ہوا'; + + @override + String get savedUsingLocalStorage => + 'مقامی ذخیرہ کار استعمال کر کے محفوظ ہوا'; + + @override + String get errorWhileSavingImage => 'تصویر کو محفوظ کرتے وقت خطا'; + + @override + String get pleaseEnterTextForYourLink => + "براہ کرم اپنے لنک کے لیے متن درج کریں (مثال کے طور پر، 'مزید جانیں')"; + + @override + String get pleaseEnterTheLinkURL => + "براہ کرم لنک کا URL درج کریں (مثال کے طور پر، 'https://example.com')"; + + @override + String get pleaseEnterAValidImageURL => + 'براہ کرم ایک درست تصویر URL درج کریں'; + + @override + String get pleaseEnterAValidVideoURL => + 'براہ کرم ایک درست ویڈیو URL درج کریں'; + + @override + String get photo => 'تصویر'; + + @override + String get image => 'تصویر'; + + @override + String get caseSensitivityAndWholeWordSearch => + 'معاملے کی حساسیت اور پورے الفاظ کی تلاش'; + + @override + String get insertImage => 'تصویر داخل کریں'; +} diff --git a/lib/src/l10n/generated/quill_localizations_vi.dart b/lib/src/l10n/generated/quill_localizations_vi.dart new file mode 100644 index 00000000..439d1de5 --- /dev/null +++ b/lib/src/l10n/generated/quill_localizations_vi.dart @@ -0,0 +1,231 @@ +import 'quill_localizations.dart'; + +/// The translations for Vietnamese (`vi`). +class FlutterQuillLocalizationsVi extends FlutterQuillLocalizations { + FlutterQuillLocalizationsVi([super.locale = 'vi']); + + @override + String get pasteLink => 'Chèn liên kết'; + + @override + String get ok => 'Đồng ý'; + + @override + String get selectColor => 'Chọn Màu'; + + @override + String get gallery => 'Thư viện'; + + @override + String get link => 'Liên kết'; + + @override + String get open => 'Mở'; + + @override + String get copy => 'Sao chép'; + + @override + String get remove => 'Xoá'; + + @override + String get save => 'Lưu'; + + @override + String get zoom => 'Thu phóng'; + + @override + String get saved => 'Đã lưu'; + + @override + String get text => 'Chữ'; + + @override + String get resize => 'Resize'; + + @override + String get width => 'Rộng'; + + @override + String get height => 'Cao'; + + @override + String get size => 'Kích thước'; + + @override + String get small => 'Nhỏ'; + + @override + String get large => 'Lớn'; + + @override + String get huge => 'Rất lớn'; + + @override + String get clear => 'Xoá'; + + @override + String get font => 'Phông chữ'; + + @override + String get search => 'Tìm'; + + @override + String get camera => 'Máy ảnh'; + + @override + String get video => 'Video'; + + @override + String get undo => 'Hoàn tác'; + + @override + String get redo => 'Làm lại'; + + @override + String get fontFamily => 'Phông chữ'; + + @override + String get fontSize => 'Cỡ chữ'; + + @override + String get bold => 'Đậm'; + + @override + String get subscript => 'Chèn dưới'; + + @override + String get superscript => 'Chèn trên'; + + @override + String get italic => 'Nghiêng'; + + @override + String get underline => 'Gạch chân'; + + @override + String get strikeThrough => 'Gạch ngang'; + + @override + String get inlineCode => 'Dòng mã'; + + @override + String get fontColor => 'Màu chữ'; + + @override + String get backgroundColor => 'Màu nền'; + + @override + String get clearFormat => 'Xoá định dạng'; + + @override + String get alignLeft => 'Căn trái'; + + @override + String get alignCenter => 'Căn giữa'; + + @override + String get alignRight => 'Căn phải'; + + @override + String get justifyWinWidth => 'Căn đều chiều rộng'; + + @override + String get textDirection => 'Hướng văn bản'; + + @override + String get headerStyle => 'Kiểu tiêu đề'; + + @override + String get numberedList => 'Danh sách có số'; + + @override + String get bulletList => 'Danh sách định dạng'; + + @override + String get checkedList => 'Danh sách kiểm tra'; + + @override + String get codeBlock => 'Khối mã'; + + @override + String get quote => 'Trích dẫn'; + + @override + String get increaseIndent => 'Tăng độ lề'; + + @override + String get decreaseIndent => 'Giảm độ lề'; + + @override + String get insertURL => 'Chèn URL'; + + @override + String get visitLink => 'Truy cập liên kết'; + + @override + String get enterLink => 'Nhập liên kết'; + + @override + String get enterMedia => 'Chèn phương tiện'; + + @override + String get edit => 'Chỉnh sửa'; + + @override + String get apply => 'Áp dụng'; + + @override + String get hex => 'Hex'; + + @override + String get material => 'Chất liệu'; + + @override + String get color => 'Màu'; + + @override + String get findText => 'Tìm văn bản'; + + @override + String get moveToPreviousOccurrence => 'Di chuyển đến lần xuất hiện trước'; + + @override + String get moveToNextOccurrence => 'Di chuyển đến lần xuất hiện tiếp theo'; + + @override + String get savedUsingTheNetwork => 'Đã lưu bằng cách sử dụng mạng'; + + @override + String get savedUsingLocalStorage => 'Đã lưu sử dụng lưu trữ địa phương'; + + @override + String get errorWhileSavingImage => 'Lỗi khi lưu hình ảnh'; + + @override + String get pleaseEnterTextForYourLink => + "Vui lòng nhập văn bản cho liên kết của bạn (ví dụ: 'Tìm hiểu thêm')"; + + @override + String get pleaseEnterTheLinkURL => + "Vui lòng nhập URL của liên kết (ví dụ: 'https://example.com')"; + + @override + String get pleaseEnterAValidImageURL => 'Vui lòng nhập URL hình ảnh hợp lệ'; + + @override + String get pleaseEnterAValidVideoURL => 'Vui lòng nhập URL video hợp lệ'; + + @override + String get photo => 'Ảnh'; + + @override + String get image => 'Hình ảnh'; + + @override + String get caseSensitivityAndWholeWordSearch => + 'Độ nhạy cảm chữ hoa/chữ thường và tìm kiếm toàn bộ từ'; + + @override + String get insertImage => 'Chèn hình ảnh'; +} diff --git a/lib/src/l10n/generated/quill_localizations_zh.dart b/lib/src/l10n/generated/quill_localizations_zh.dart new file mode 100644 index 00000000..5c7a4232 --- /dev/null +++ b/lib/src/l10n/generated/quill_localizations_zh.dart @@ -0,0 +1,682 @@ +import 'quill_localizations.dart'; + +/// The translations for Chinese (`zh`). +class FlutterQuillLocalizationsZh extends FlutterQuillLocalizations { + FlutterQuillLocalizationsZh([super.locale = 'zh']); + + @override + String get pasteLink => '粘贴链接'; + + @override + String get ok => '确定'; + + @override + String get selectColor => '选择颜色'; + + @override + String get gallery => '相册'; + + @override + String get link => '链接'; + + @override + String get open => '打开'; + + @override + String get copy => '复制'; + + @override + String get remove => '移除'; + + @override + String get save => '保存'; + + @override + String get zoom => '缩放'; + + @override + String get saved => '已保存'; + + @override + String get text => '文本'; + + @override + String get resize => '调整大小'; + + @override + String get width => '宽度'; + + @override + String get height => '高度'; + + @override + String get size => '大小'; + + @override + String get small => '小'; + + @override + String get large => '大'; + + @override + String get huge => '巨大'; + + @override + String get clear => '清除'; + + @override + String get font => '字体'; + + @override + String get search => '搜索'; + + @override + String get camera => '相机'; + + @override + String get video => '视频'; + + @override + String get undo => '撤销'; + + @override + String get redo => '重做'; + + @override + String get fontFamily => '字体族'; + + @override + String get fontSize => '字号'; + + @override + String get bold => '加粗'; + + @override + String get subscript => '下标'; + + @override + String get superscript => '上标'; + + @override + String get italic => '斜体'; + + @override + String get underline => '下划线'; + + @override + String get strikeThrough => '删除线'; + + @override + String get inlineCode => '行内代码'; + + @override + String get fontColor => '字体颜色'; + + @override + String get backgroundColor => '背景颜色'; + + @override + String get clearFormat => '清除格式'; + + @override + String get alignLeft => '左对齐'; + + @override + String get alignCenter => '居中'; + + @override + String get alignRight => '右对齐'; + + @override + String get justifyWinWidth => '两端对齐'; + + @override + String get textDirection => '文本方向'; + + @override + String get headerStyle => '标题样式'; + + @override + String get numberedList => '编号列表'; + + @override + String get bulletList => '项目符号列表'; + + @override + String get checkedList => '选中列表'; + + @override + String get codeBlock => '代码块'; + + @override + String get quote => '引用'; + + @override + String get increaseIndent => '增加缩进'; + + @override + String get decreaseIndent => '减少缩进'; + + @override + String get insertURL => '插入网址'; + + @override + String get visitLink => '访问链接'; + + @override + String get enterLink => '输入链接'; + + @override + String get enterMedia => '输入媒体'; + + @override + String get edit => '编辑'; + + @override + String get apply => '应用'; + + @override + String get hex => '十六进制'; + + @override + String get material => '素材'; + + @override + String get color => '颜色'; + + @override + String get findText => '查找文本'; + + @override + String get moveToPreviousOccurrence => '移到前一个匹配项'; + + @override + String get moveToNextOccurrence => '移到下一个匹配项'; + + @override + String get savedUsingTheNetwork => '使用网络保存'; + + @override + String get savedUsingLocalStorage => '使用本地存储保存'; + + @override + String get errorWhileSavingImage => '保存图像时出错'; + + @override + String get pleaseEnterTextForYourLink => "请输入链接文本(例如,'了解更多')"; + + @override + String get pleaseEnterTheLinkURL => "请输入链接网址(例如,'https://example.com')"; + + @override + String get pleaseEnterAValidImageURL => '请输入有效的图像网址'; + + @override + String get pleaseEnterAValidVideoURL => '请输入有效的视频URL'; + + @override + String get photo => '照片'; + + @override + String get image => '图像'; + + @override + String get caseSensitivityAndWholeWordSearch => '区分大小写和整词搜索'; + + @override + String get insertImage => '插入图像'; +} + +/// The translations for Chinese, as used in China (`zh_CN`). +class FlutterQuillLocalizationsZhCn extends FlutterQuillLocalizationsZh { + FlutterQuillLocalizationsZhCn() : super('zh_CN'); + + @override + String get pasteLink => '粘贴链接'; + + @override + String get ok => '好'; + + @override + String get selectColor => '选择颜色'; + + @override + String get gallery => '相簿'; + + @override + String get link => '链接'; + + @override + String get open => '打开'; + + @override + String get copy => '复制'; + + @override + String get remove => '移除'; + + @override + String get save => '保存'; + + @override + String get zoom => '放大'; + + @override + String get saved => '已保存'; + + @override + String get text => '文字'; + + @override + String get resize => '调整大小'; + + @override + String get width => '宽度'; + + @override + String get height => '高度'; + + @override + String get size => '文字大小'; + + @override + String get small => '小字号'; + + @override + String get large => '大字号'; + + @override + String get huge => '超大字号'; + + @override + String get clear => '清除'; + + @override + String get font => '字体'; + + @override + String get search => '搜索'; + + @override + String get camera => '拍照'; + + @override + String get video => '录像'; + + @override + String get undo => '撤销'; + + @override + String get redo => '重做'; + + @override + String get fontFamily => '字体'; + + @override + String get fontSize => '字号'; + + @override + String get bold => '粗体'; + + @override + String get subscript => '下标'; + + @override + String get superscript => '上标'; + + @override + String get italic => '斜体'; + + @override + String get underline => '下划线'; + + @override + String get strikeThrough => '删除线'; + + @override + String get inlineCode => '内联代码'; + + @override + String get fontColor => '字体颜色'; + + @override + String get backgroundColor => '背景颜色'; + + @override + String get clearFormat => '清除格式'; + + @override + String get alignLeft => '左对齐'; + + @override + String get alignCenter => '居中对齐'; + + @override + String get alignRight => '右对齐'; + + @override + String get justifyWinWidth => '两端对齐'; + + @override + String get textDirection => '文本方向'; + + @override + String get headerStyle => '标题样式'; + + @override + String get numberedList => '有序列表'; + + @override + String get bulletList => '无序列表'; + + @override + String get checkedList => '任务列表'; + + @override + String get codeBlock => '代码块'; + + @override + String get quote => '引言'; + + @override + String get increaseIndent => '增加缩进'; + + @override + String get decreaseIndent => '减少缩进'; + + @override + String get insertURL => '插入链接'; + + @override + String get visitLink => '访问链接'; + + @override + String get enterLink => '输入链接'; + + @override + String get enterMedia => '输入媒体'; + + @override + String get edit => '编辑'; + + @override + String get apply => '应用'; + + @override + String get hex => '十六进制'; + + @override + String get material => '材料'; + + @override + String get color => '颜色'; + + @override + String get findText => '搜索文本'; + + @override + String get moveToPreviousOccurrence => '上一个匹配项'; + + @override + String get moveToNextOccurrence => '下一个匹配项'; + + @override + String get savedUsingTheNetwork => '通过网络保存'; + + @override + String get savedUsingLocalStorage => '使用本地存储保存'; + + @override + String get errorWhileSavingImage => '保存图像时发生错误'; + + @override + String get pleaseEnterTextForYourLink => "例如,'了解更多'"; + + @override + String get pleaseEnterTheLinkURL => "例如,'https://example.com'"; + + @override + String get pleaseEnterAValidImageURL => '请输入有效的图像URL'; + + @override + String get pleaseEnterAValidVideoURL => '请输入有效的视频URL'; + + @override + String get photo => '照片'; + + @override + String get image => '图像'; + + @override + String get caseSensitivityAndWholeWordSearch => '区分大小写和整词搜索'; + + @override + String get insertImage => '插入图像'; +} + +/// The translations for Chinese, as used in Hong Kong (`zh_HK`). +class FlutterQuillLocalizationsZhHk extends FlutterQuillLocalizationsZh { + FlutterQuillLocalizationsZhHk() : super('zh_HK'); + + @override + String get pasteLink => '貼上連結'; + + @override + String get ok => '確定'; + + @override + String get selectColor => '選擇顏色'; + + @override + String get gallery => '圖片庫'; + + @override + String get link => '連結'; + + @override + String get open => '開啓'; + + @override + String get copy => '複製'; + + @override + String get remove => '移除'; + + @override + String get save => '儲存'; + + @override + String get zoom => '放大'; + + @override + String get saved => '已儲存'; + + @override + String get text => '文字'; + + @override + String get resize => '變更大小'; + + @override + String get width => '寛'; + + @override + String get height => '高'; + + @override + String get size => '大小'; + + @override + String get small => '小'; + + @override + String get large => '大'; + + @override + String get huge => '超大'; + + @override + String get clear => '清除'; + + @override + String get font => '字型'; + + @override + String get search => '搜尋'; + + @override + String get camera => '相機'; + + @override + String get video => '錄影'; + + @override + String get undo => '撤銷'; + + @override + String get redo => '重做'; + + @override + String get fontFamily => '字體'; + + @override + String get fontSize => '字號'; + + @override + String get bold => '粗體'; + + @override + String get subscript => '下標'; + + @override + String get superscript => '上標'; + + @override + String get italic => '斜體'; + + @override + String get underline => '下劃線'; + + @override + String get strikeThrough => '刪除線'; + + @override + String get inlineCode => '內聯代碼'; + + @override + String get fontColor => '字體顏色'; + + @override + String get backgroundColor => '背景顏色'; + + @override + String get clearFormat => '清除格式'; + + @override + String get alignLeft => '左對齊'; + + @override + String get alignCenter => '居中對齊'; + + @override + String get alignRight => '右對齊'; + + @override + String get justifyWinWidth => '兩端對齊'; + + @override + String get textDirection => '文本方向'; + + @override + String get headerStyle => '標題樣式'; + + @override + String get numberedList => '有序列表'; + + @override + String get bulletList => '無序列表'; + + @override + String get checkedList => '任務列表'; + + @override + String get codeBlock => '代碼塊'; + + @override + String get quote => '引言'; + + @override + String get increaseIndent => '增加縮進'; + + @override + String get decreaseIndent => '減少縮進'; + + @override + String get insertURL => '插入鏈接'; + + @override + String get visitLink => '訪問鏈接'; + + @override + String get enterLink => '輸入鏈接'; + + @override + String get enterMedia => '輸入媒體'; + + @override + String get edit => '編輯'; + + @override + String get apply => '應用'; + + @override + String get hex => '十六進制'; + + @override + String get material => '物料'; + + @override + String get color => '顏色'; + + @override + String get findText => '搜尋文本'; + + @override + String get moveToPreviousOccurrence => '上一個匹配項'; + + @override + String get moveToNextOccurrence => '下一個匹配項'; + + @override + String get savedUsingTheNetwork => '通過網絡保存'; + + @override + String get savedUsingLocalStorage => '使用本地存儲保存'; + + @override + String get errorWhileSavingImage => '保存圖像時發生錯誤'; + + @override + String get pleaseEnterTextForYourLink => "例如,'了解更多'"; + + @override + String get pleaseEnterTheLinkURL => "例如,'https://example.com'"; + + @override + String get pleaseEnterAValidImageURL => '請輸入有效的圖像URL'; + + @override + String get pleaseEnterAValidVideoURL => '請輸入有效的視頻URL'; + + @override + String get photo => '照片'; + + @override + String get image => '圖像'; + + @override + String get caseSensitivityAndWholeWordSearch => '區分大小寫和整詞搜索'; + + @override + String get insertImage => '插入圖像'; +} diff --git a/lib/src/l10n/quill_ar.arb b/lib/src/l10n/quill_ar.arb new file mode 100644 index 00000000..dd2e0911 --- /dev/null +++ b/lib/src/l10n/quill_ar.arb @@ -0,0 +1,78 @@ +{ + "@@locale": "ar", + "pasteLink": "نسخ الرابط", + "ok": "نعم", + "selectColor": "اختار اللون", + "gallery": "المعرض", + "link": "الرابط", + "open": "فتح", + "copy": "نسخ", + "remove": "إزالة", + "save": "حفظ", + "zoom": "تكبير", + "saved": "تم الحفظ", + "text": "نص", + "resize": "تحجيم", + "width": "عرض", + "height": "ارتفاع", + "size": "حجم", + "small": "صغير", + "large": "كبير", + "huge": "ضخم", + "clear": "تنظيف", + "font": "خط", + "search": "بحث", + "camera": "كاميرا", + "video": "فيديو", + "undo": "تراجع", + "redo": "تقدم", + "fontFamily": "عائلة الخط", + "fontSize": "حجم الخط", + "bold": "عريض", + "subscript": "نص سفلي", + "superscript": "نص علوي", + "italic": "مائل", + "underline": "تحته خط", + "strikeThrough": "داخله خط", + "inlineCode": "كود بوسط السطر", + "fontColor": "لون الخط", + "backgroundColor": "لون الخلفية", + "clearFormat": "تنظيف التنسيق", + "alignLeft": "محاذاة اليسار", + "alignCenter": "محاذاة الوسط", + "alignRight": "محاذاة اليمين", + "justifyWinWidth": "تبرير مع العرض", + "textDirection": "اتجاه النص", + "headerStyle": "ستايل العنوان", + "numberedList": "قائمة مرقمة", + "bulletList": "قائمة منقطة", + "checkedList": "قائمة للمهام", + "codeBlock": "كود كامل", + "quote": "اقتباس", + "increaseIndent": "زيادة الهامش", + "decreaseIndent": "تنقيص الهامش", + "insertURL": "ادخل عنوان رابط", + "visitLink": "زيارة الرابط", + "enterLink": "ادخل رابط", + "enterMedia": "ادخل وسائط", + "edit": "تعديل", + "apply": "تطبيق", + "hex": "Hex", + "material": "Material", + "color": "اللون", + "findText": "بحث عن نص", + "moveToPreviousOccurrence": "الانتقال إلى الحدث السابق", + "moveToNextOccurrence": "الانتقال إلى الحدث التالي", + "savedUsingTheNetwork": "تم الحفظ باستخدام الشبكة", + "savedUsingLocalStorage": "تم الحفظ باستخدام وحدة التخزين المحلية", + "errorWhileSavingImage": "حدث خطأ أثناء حفظ الصورة", + "pleaseEnterTextForYourLink": "مثال: 'تعلم المزيد'", + "pleaseEnterTheLinkURL": "مثال: 'https://example.com'", + "pleaseEnterAValidImageURL": "الرجاء إدخال عنوان URL صحيح للصورة", + "pleaseEnterAValidVideoURL": "الرجاء إدخال عنوان URL صالح للفيديو", + "photo": "صورة", + "image": "صورة", + "caseSensitivityAndWholeWordSearch": "حالة الحساسية والبحث عن كلمة كاملة", + "insertImage": "إدراج صورة" +} + \ No newline at end of file diff --git a/lib/src/l10n/quill_bg.arb b/lib/src/l10n/quill_bg.arb new file mode 100644 index 00000000..d4fa9297 --- /dev/null +++ b/lib/src/l10n/quill_bg.arb @@ -0,0 +1,79 @@ +{ + "@@locale": "bg", + "pasteLink": "Поставете връзка", + "ok": "Да", + "selectColor": "Изберете цвят", + "gallery": "Галерия", + "link": "Връзка", + "open": "Отвори", + "copy": "Копирай", + "remove": "Премахни", + "save": "Запази", + "zoom": "Увеличи", + "saved": "Запазено", + "text": "Текст", + "resize": "Промяна на размера", + "width": "Ширина", + "height": "Височина", + "size": "Размер", + "small": "Малък", + "large": "Голям", + "huge": "Огромен", + "clear": "Изчисти", + "font": "Шрифт", + "search": "Търси", + "camera": "Камера", + "video": "Видео", + "undo": "Отмени", + "redo": "Възстанови", + "fontFamily": "Шрифт", + "fontSize": "Размер на шрифта", + "bold": "Получер", + "subscript": "Индекс", + "superscript": "Надпис", + "italic": "Курсив", + "underline": "Подчертан", + "strikeThrough": "Зачертан", + "inlineCode": "Вграден код", + "fontColor": "Цвят на шрифта", + "backgroundColor": "Цвят на фона", + "clearFormat": "Изчисти формат", + "alignLeft": "Подравни вляво", + "alignCenter": "Подравни в центъра", + "alignRight": "Подравни вдясно", + "justifyWinWidth": "Подравни във всяка колонка", + "textDirection": "Посока на текста", + "headerStyle": "Стил на заглавието", + "numberedList": "Номериран списък", + "bulletList": "Маркиран списък", + "checkedList": "Списък с отметки", + "codeBlock": "Блок с код", + "quote": "Цитат", + "increaseIndent": "Увеличи отстъпа", + "decreaseIndent": "Намали отстъпа", + "insertURL": "Вмъкни URL", + "visitLink": "Посети връзка", + "enterLink": "Въведи връзка", + "enterMedia": "Въведи медия", + "edit": "Редактирай", + "apply": "Приложи", + "hex": "Hex", + "material": "Material", + "color": "Цвят", + "findText": "Намери текст", + "moveToPreviousOccurrence": "Премести към предишното съвпадение", + "moveToNextOccurrence": "Премести към следващото съвпадение", + "savedUsingNetwork": "Saved using the network", + "savedUsingLocalStorage": "Saved using the local storage", + "errorWhileSavingImage": "Error while saving image", + "pleaseEnterTextForYourLink": "Например, 'Научете повече'", + "pleaseEnterTheLinkURL": "Например, 'https://example.com'", + "pleaseEnterAValidImageURL": "Моля, въведете валиден URL на изображението", + "savedUsingTheNetwork": "Запазено с помощта на мрежата", + "pleaseEnterAValidVideoURL": "Моля, въведете валиден URL адрес за видео", + "photo": "Снимка", + "image": "Изображение", + "caseSensitivityAndWholeWordSearch": "Чувствителност на кутията и търсене на цялата дума", + "insertImage": "Вмъкване на изображение" + } + \ No newline at end of file diff --git a/lib/src/l10n/quill_bn.arb b/lib/src/l10n/quill_bn.arb new file mode 100644 index 00000000..b709ec06 --- /dev/null +++ b/lib/src/l10n/quill_bn.arb @@ -0,0 +1,82 @@ +{ + "@@locale": "bn", + "pasteLink": "লিঙ্ক পেস্ট করুন", + "ok": "ওকে", + "selectColor": "কালার সিলেক্ট করুন", + "gallery": "গ্যালারি", + "link": "লিঙ্ক", + "open": "ওপেন", + "copy": "কপি", + "remove": "রিমুভ", + "save": "সেভ", + "zoom": "জুম", + "saved": "সেভড", + "text": "টেক্সট", + "resize": "রিসাইজ", + "width": "প্রস্থ", + "height": "দৈর্ঘ্য", + "size": "সাইজ", + "small": "ছোট", + "large": "বড়", + "huge": "বিশাল", + "clear": "ক্লিয়ার", + "font": "ফন্ট", + "search": "সার্চ", + "camera": "ক্যামেরা", + "video": "ভিডিও", + "undo": "আন্ডু", + "redo": "রিডু", + "fontFamily": "ফন্ট ফ্যামিলি", + "fontSize": "ফন্ট সাইজ", + "bold": "বোল্ড", + "subscript": "সাবস্ক্রিপ্ট", + "superscript": "সুপারস্ক্রিপ্ট", + "italic": "ইটালিক", + "underline": "আন্ডারলাইন", + "strikeThrough": "স্ট্রাইক থ্রু", + "inlineCode": "ইনলাইন কোড", + "fontColor": "ফন্ট কালার", + "backgroundColor": "ব্যাকগ্রাউন্ড কালার", + "clearFormat": "ক্লিয়ার ফরম্যাট", + "alignLeft": "বাম সারিবদ্ধ", + "alignCenter": "কেন্দ্র সারিবদ্ধ", + "alignRight": "ডান সারিবদ্ধ", + "justifyWinWidth": "প্রস্থের সাথে সংযত", + "textDirection": "টেক্সট ডিরেকশন", + "headerStyle": "হেডার স্টাইল", + "numberedList": "সংখ্যাযুক্ত তালিকা", + "bulletList": "বুলেট তালিকা", + "checkedList": "চেক করা তালিকা", + "codeBlock": "কোড ব্লক", + "quote": "উক্তি", + "increaseIndent": "ইন্ডেন্ট বাড়ান", + "decreaseIndent": "ইন্ডেন্ট কমান", + "insertURL": "UR দিন", + "visitLink": "ভিজিট লিঙ্ক", + "enterLink": "লিঙ্ক দিন", + "enterMedia": "মিডিয়া দিন", + "edit": "ইডিট", + "apply": "এপ্লাই", + "hex": "হেক্স", + "material": "ম্যাটারিয়াল", + "color": "কালার", + "findText": "পাঠ্য খুঁজুন", + "moveToPreviousOccurrence": "পূর্ববর্তী ঘটনায় চলুন", + "moveToNextOccurrence": "পরবর্তী ঘটনায় চলুন", + "savedUsingNetwork": "নেটওয়ার্ক ব্যবহার করে সংরক্ষিত", + "savedUsingLocalStorage": "স্থানীয় সংরক্ষণ ব্যবহার করে সংরক্ষিত", + "errorWhileSavingImage": "চিত্র সংরক্ষণে সময়ে ত্রুটি", + "enterTextForYourLink": "আপনার লিঙ্কের জন্য একটি টেক্সট লিখুন, উদাহরণস্বরূপ, 'আরও জানুন'", + "enterLinkURL": "আপনার লিঙ্ক URL লিখুন, উদাহরণস্বরূপ, 'https://example.com'", + "enterValidImageURL": "একটি বৈধ চিত্র URL লিখুন", + "savedUsingTheNetwork": "নেটওয়ার্ক ব্যবহার করে সংরক্ষিত", + "pleaseEnterTextForYourLink": "আপনার লিঙ্কের জন্য একটি টেক্সট লিখুন (উদাঃ 'আরও জানুন')", + "pleaseEnterTheLinkURL": "দয়া করে লিঙ্ক URL লিখুন (উদাঃ 'https://example.com')", + "pleaseEnterAValidImageURL": "দয়া করে একটি বৈধ চিত্র URL লিখুন", + "pleaseEnterAValidVideoURL": "দয়া করে একটি বৈধ ভিডিও URL লিখুন", + "photo": "ফটো", + "image": "চিত্র", + "caseSensitivityAndWholeWordSearch": "কেস সেন্সিটিভিটি এবং পূর্ণ শব্দ অনুসন্ধান", + "insertImage": "চিত্র সন্নিবেশ" + } + \ No newline at end of file diff --git a/lib/src/l10n/quill_cs.arb b/lib/src/l10n/quill_cs.arb new file mode 100644 index 00000000..a009adab --- /dev/null +++ b/lib/src/l10n/quill_cs.arb @@ -0,0 +1,82 @@ +{ + "@@locale": "cs", + "pasteLink": "Vložit odkaz", + "ok": "Ok", + "selectColor": "Vybrat barvu", + "gallery": "Galerie", + "link": "Odkaz", + "open": "Otevřít", + "copy": "Kopírovat", + "remove": "Odstranit", + "save": "Uložit", + "zoom": "Přiblížit", + "saved": "Uloženo", + "text": "Text", + "resize": "Změnit velikost", + "width": "Šířka", + "height": "Výška", + "size": "Velikost", + "small": "Malý", + "large": "Velký", + "huge": "Obrovský", + "clear": "Smazat", + "font": "Písmo", + "search": "Hledat", + "camera": "Kamera", + "video": "Video", + "undo": "Zpět", + "redo": "Znovu", + "fontFamily": "Rodina písma", + "fontSize": "Velikost písma", + "bold": "Tučné", + "subscript": "Dolní index", + "superscript": "Horní index", + "italic": "Kurzíva", + "underline": "Podtržení", + "strikeThrough": "Přeškrtnuté", + "inlineCode": "Inline kód", + "fontColor": "Barva písma", + "backgroundColor": "Barva pozadí", + "clearFormat": "Vymazat formátování", + "alignLeft": "Zarovnat vlevo", + "alignCenter": "Zarovnat na střed", + "alignRight": "Zarovnat vpravo", + "justifyWinWidth": "Zarovnat do bloku", + "textDirection": "Směr textu", + "headerStyle": "Styl záhlaví", + "numberedList": "Číslovaný seznam", + "bulletList": "Seznam s odrážkami", + "checkedList": "Seznam s zaškrtávacími políčky", + "codeBlock": "Blokový kód", + "quote": "Citace", + "increaseIndent": "Zvětšit odsazení", + "decreaseIndent": "Zmenšit odsazení", + "insertURL": "Vložit URL", + "visitLink": "Otevřít odkaz", + "enterLink": "Vložit odkaz", + "enterMedia": "Vložit média", + "edit": "Upravit", + "apply": "Použít", + "hex": "Hex", + "material": "Material", + "color": "Barva", + "findText": "Najít text", + "moveToPreviousOccurrence": "Přesunout na předchozí výskyt", + "moveToNextOccurrence": "Přesunout na následující výskyt", + "savedUsingNetwork": "Uloženo pomocí sítě", + "savedUsingLocalStorage": "Uloženo lokálně", + "errorWhileSavingImage": "Chyba při ukládání obrázku", + "enterTextForYourLink": "Například 'Zjistit více'", + "enterLinkURL": "Například 'https://example.com'", + "enterValidImageURL": "Vložte platný URL obrázku", + "savedUsingTheNetwork": "Uloženo pomocí sítě", + "pleaseEnterTextForYourLink": "Zadejte text pro váš odkaz (např., 'Dozvědět se více')", + "pleaseEnterTheLinkURL": "Zadejte URL odkazu (např., 'https://example.com')", + "pleaseEnterAValidImageURL": "Zadejte platnou URL adresu obrázku", + "pleaseEnterAValidVideoURL": "Zadejte platnou URL adresu videa", + "photo": "Foto", + "image": "Obrázek", + "caseSensitivityAndWholeWordSearch": "Citlivost na velká a malá písmena a vyhledávání celého slova", + "insertImage": "Vložit obrázek" + } + \ No newline at end of file diff --git a/lib/src/l10n/quill_da.arb b/lib/src/l10n/quill_da.arb new file mode 100644 index 00000000..2ca4015c --- /dev/null +++ b/lib/src/l10n/quill_da.arb @@ -0,0 +1,78 @@ +{ + "@@locale": "da", + "pasteLink": "Indsæt link", + "ok": "Ok", + "selectColor": "Vælg farve", + "gallery": "Galleri", + "link": "Link", + "open": "Åben", + "copy": "Kopi", + "remove": "Fjerne", + "save": "Gemme", + "zoom": "Zoom ind", + "saved": "Gemt", + "text": "Text", + "resize": "Resize", + "width": "Width", + "height": "Height", + "size": "Size", + "small": "Small", + "large": "Large", + "huge": "Huge", + "clear": "Clear", + "font": "Font", + "search": "Search", + "camera": "Camera", + "video": "Video", + "undo": "Undo", + "redo": "Redo", + "fontFamily": "Font family", + "fontSize": "Font size", + "bold": "Bold", + "subscript": "Subscript", + "superscript": "Superscript", + "italic": "Italic", + "underline": "Underline", + "strikeThrough": "Strike through", + "inlineCode": "Inline code", + "fontColor": "Font color", + "backgroundColor": "Background color", + "clearFormat": "Clear format", + "alignLeft": "Align left", + "alignCenter": "Align center", + "alignRight": "Align right", + "justifyWinWidth": "Justify win width", + "textDirection": "Text direction", + "headerStyle": "Header style", + "numberedList": "Numbered list", + "bulletList": "Bullet list", + "checkedList": "Checked list", + "codeBlock": "Code block", + "quote": "Quote", + "increaseIndent": "Increase indent", + "decreaseIndent": "Decrease indent", + "insertURL": "Insert URL", + "visitLink": "Visit link", + "enterLink": "Enter link", + "enterMedia": "Enter media", + "edit": "Edit", + "apply": "Apply", + "findText": "Find text", + "moveToPreviousOccurrence": "Move to previous occurrence", + "moveToNextOccurrence": "Move to next occurrence", + "savedUsingTheNetwork": "Saved using the network", + "savedUsingLocalStorage": "Saved using the local storage", + "errorWhileSavingImage": "Error while saving image", + "pleaseEnterTextForYourLink": "e.g., 'Learn more'", + "pleaseEnterTheLinkURL": "e.g., 'https://example.com'", + "pleaseEnterAValidImageURL": "Please enter a valid image URL", + "hex": "Hex", + "material": "Materiale", + "color": "Farve", + "pleaseEnterAValidVideoURL": "Angiv en gyldig video-URL", + "photo": "Foto", + "image": "Billede", + "caseSensitivityAndWholeWordSearch": "Stor- og småbogstavsfølsomhed samt helordsøgning", + "insertImage": "Indsæt billede" + } + \ No newline at end of file diff --git a/lib/src/l10n/quill_de.arb b/lib/src/l10n/quill_de.arb new file mode 100644 index 00000000..a8e080ef --- /dev/null +++ b/lib/src/l10n/quill_de.arb @@ -0,0 +1,78 @@ +{ + "@@locale": "de", + "pasteLink": "Link hinzufügen", + "ok": "OK", + "selectColor": "Farbe auswählen", + "gallery": "Galerie", + "link": "Link", + "open": "Öffnen", + "copy": "Kopieren", + "remove": "Entfernen", + "save": "Speichern", + "zoom": "Zoomen", + "saved": "Gespeichert", + "text": "Text", + "resize": "Größe ändern", + "width": "Breite", + "height": "Höhe", + "size": "Größe", + "small": "Klein", + "large": "Groß", + "huge": "Riesig", + "clear": "Löschen", + "font": "Schrift", + "search": "Suchen", + "camera": "Kamera", + "video": "Video", + "undo": "Rückgängig", + "redo": "Wiederherstellen", + "fontFamily": "Schriftart", + "fontSize": "Schriftgröße", + "bold": "Fett", + "subscript": "Tiefgestellt", + "superscript": "Hochgestellt", + "italic": "Kursiv", + "underline": "Unterstreichen", + "strikeThrough": "Durchstreichen", + "inlineCode": "Inline-Code", + "fontColor": "Schriftfarbe", + "backgroundColor": "Hintergrundfarbe", + "clearFormat": "Formatierung löschen", + "alignLeft": "Linksbündig ausrichten", + "alignCenter": "Zentriert ausrichten", + "alignRight": "Rechtsbündig ausrichten", + "justifyWinWidth": "Blocksatz", + "textDirection": "Textrichtung", + "headerStyle": "Überschrift-Stil", + "numberedList": "Nummerierte Liste", + "bulletList": "Aufzählungsliste", + "checkedList": "Checkliste", + "codeBlock": "Code-Block", + "quote": "Zitat", + "increaseIndent": "Einzug vergrößern", + "decreaseIndent": "Einzug verkleinern", + "insertURL": "URL einfügen", + "visitLink": "Link öffnen", + "enterLink": "Link eingeben", + "enterMedia": "Medien einfügen", + "edit": "Bearbeiten", + "apply": "Anwenden", + "findText": "Text suchen", + "moveToPreviousOccurrence": "Zum vorherigen Auftreten springen", + "moveToNextOccurrence": "Zum nächsten Auftreten springen", + "savedUsingTheNetwork": "Saved using the network", + "savedUsingLocalStorage": "Saved using the local storage", + "errorWhileSavingImage": "Error while saving image", + "pleaseEnterTextForYourLink": "e.g., 'Learn more'", + "pleaseEnterTheLinkURL": "e.g., 'https://example.com'", + "pleaseEnterAValidImageURL": "Please enter a valid image URL", + "hex": "Hex", + "material": "Material", + "color": "Farbe", + "pleaseEnterAValidVideoURL": "Bitte geben Sie eine gültige Video-URL ein", + "photo": "Foto", + "image": "Bild", + "caseSensitivityAndWholeWordSearch": "Groß- und Kleinschreibung sowie Ganzwortsuche", + "insertImage": "Bild einfügen" + } + \ No newline at end of file diff --git a/lib/src/l10n/quill_en.arb b/lib/src/l10n/quill_en.arb new file mode 100644 index 00000000..c0ec7967 --- /dev/null +++ b/lib/src/l10n/quill_en.arb @@ -0,0 +1,77 @@ +{ + "@@locale": "en", + "pasteLink": "Paste a link", + "ok": "Ok", + "selectColor": "Select Color", + "gallery": "Gallery", + "link": "Link", + "open": "Open", + "copy": "Copy", + "remove": "Remove", + "save": "Save", + "zoom": "Zoom", + "saved": "Saved", + "text": "Text", + "resize": "Resize", + "width": "Width", + "height": "Height", + "size": "Size", + "small": "Small", + "large": "Large", + "huge": "Huge", + "clear": "Clear", + "font": "Font", + "search": "Search", + "camera": "Camera", + "video": "Video", + "undo": "Undo", + "redo": "Redo", + "fontFamily": "Font family", + "fontSize": "Font size", + "bold": "Bold", + "subscript": "Subscript", + "superscript": "Superscript", + "italic": "Italic", + "underline": "Underline", + "strikeThrough": "Strike through", + "inlineCode": "Inline code", + "fontColor": "Font color", + "backgroundColor": "Background color", + "clearFormat": "Clear format", + "alignLeft": "Align left", + "alignCenter": "Align center", + "alignRight": "Align right", + "justifyWinWidth": "Justify win width", + "textDirection": "Text direction", + "headerStyle": "Header style", + "numberedList": "Numbered list", + "bulletList": "Bullet list", + "checkedList": "Checked list", + "codeBlock": "Code block", + "quote": "Quote", + "increaseIndent": "Increase indent", + "decreaseIndent": "Decrease indent", + "insertURL": "Insert URL", + "visitLink": "Visit link", + "enterLink": "Enter link", + "enterMedia": "Enter media", + "edit": "Edit", + "apply": "Apply", + "hex": "Hex", + "material": "Material", + "color": "Color", + "findText": "Find text", + "moveToPreviousOccurrence": "Move to previous occurrence", + "moveToNextOccurrence": "Move to next occurrence", + "savedUsingTheNetwork": "Saved using the network", + "savedUsingLocalStorage": "Saved using the local storage", + "errorWhileSavingImage": "Error while saving image", + "pleaseEnterTextForYourLink": "Please enter a text for your link (e.g., 'Learn more')", + "pleaseEnterTheLinkURL": "Please enter the link URL (e.g., 'https://example.com')", + "pleaseEnterAValidImageURL": "Please enter a valid image URL", + "pleaseEnterAValidVideoURL": "Please enter a valid video url", + "photo": "Photo", + "image": "Image", + "caseSensitivityAndWholeWordSearch": "Case sensitivity and whole word search", + "insertImage": "Insert image" +} diff --git a/lib/src/l10n/quill_en_US.arb b/lib/src/l10n/quill_en_US.arb new file mode 100644 index 00000000..38bca0bc --- /dev/null +++ b/lib/src/l10n/quill_en_US.arb @@ -0,0 +1,77 @@ +{ + "@@locale": "en_US", + "pasteLink": "Paste a link", + "ok": "Ok", + "selectColor": "Select Color", + "gallery": "Gallery", + "link": "Link", + "open": "Open", + "copy": "Copy", + "remove": "Remove", + "save": "Save", + "zoom": "Zoom", + "saved": "Saved", + "text": "Text", + "resize": "Resize", + "width": "Width", + "height": "Height", + "size": "Size", + "small": "Small", + "large": "Large", + "huge": "Huge", + "clear": "Clear", + "font": "Font", + "search": "Search", + "camera": "Camera", + "video": "Video", + "undo": "Undo", + "redo": "Redo", + "fontFamily": "Font family", + "fontSize": "Font size", + "bold": "Bold", + "subscript": "Subscript", + "superscript": "Superscript", + "italic": "Italic", + "underline": "Underline", + "strikeThrough": "Strike through", + "inlineCode": "Inline code", + "fontColor": "Font color", + "backgroundColor": "Background color", + "clearFormat": "Clear format", + "alignLeft": "Align left", + "alignCenter": "Align center", + "alignRight": "Align right", + "justifyWinWidth": "Justify win width", + "textDirection": "Text direction", + "headerStyle": "Header style", + "numberedList": "Numbered list", + "bulletList": "Bullet list", + "checkedList": "Checked list", + "codeBlock": "Code block", + "quote": "Quote", + "increaseIndent": "Increase indent", + "decreaseIndent": "Decrease indent", + "insertURL": "Insert URL", + "visitLink": "Visit link", + "enterLink": "Enter link", + "enterMedia": "Enter media", + "edit": "Edit", + "apply": "Apply", + "hex": "Hex", + "material": "Material", + "color": "Color", + "findText": "Find text", + "moveToPreviousOccurrence": "Move to previous occurrence", + "moveToNextOccurrence": "Move to next occurrence", + "savedUsingTheNetwork": "Saved using the network", + "savedUsingLocalStorage": "Saved using the local storage", + "errorWhileSavingImage": "Error while saving image", + "pleaseEnterTextForYourLink": "Please enter a text for your link (e.g., 'Learn more')", + "pleaseEnterTheLinkURL": "Please enter the link URL (e.g., 'https://example.com')", + "pleaseEnterAValidImageURL": "Please enter a valid image URL", + "photo": "Photo", + "image": "Image", + "pleaseEnterAValidVideoURL": "Please enter a valid video URL", + "caseSensitivityAndWholeWordSearch": "Case sensitivity and whole word search", + "insertImage": "Insert Image" +} diff --git a/lib/src/l10n/quill_es.arb b/lib/src/l10n/quill_es.arb new file mode 100644 index 00000000..dd994f97 --- /dev/null +++ b/lib/src/l10n/quill_es.arb @@ -0,0 +1,78 @@ +{ + "@@locale": "es", + "pasteLink": "Pega un enlace", + "ok": "Ok", + "selectColor": "Selecciona un color", + "gallery": "Galería", + "link": "Enlace", + "open": "Abrir", + "copy": "Copiar", + "remove": "Eliminar", + "save": "Guardar", + "zoom": "Zoom", + "saved": "Guardado", + "text": "Texto", + "resize": "Redimensionar", + "width": "Ancho", + "height": "Alto", + "size": "Tamaño", + "small": "Pequeño", + "large": "Grande", + "huge": "Muy grande", + "clear": "Borrar", + "font": "Fuente", + "search": "Buscar", + "camera": "Cámara", + "video": "Vídeo", + "undo": "Undo", + "redo": "Redo", + "fontFamily": "Font family", + "fontSize": "Font size", + "bold": "Bold", + "subscript": "Subscript", + "superscript": "Superscript", + "italic": "Italic", + "underline": "Underline", + "strikeThrough": "Strike through", + "inlineCode": "Inline code", + "fontColor": "Font color", + "backgroundColor": "Background color", + "clearFormat": "Clear format", + "alignLeft": "Align left", + "alignCenter": "Align center", + "alignRight": "Align right", + "justifyWinWidth": "Justify win width", + "textDirection": "Text direction", + "headerStyle": "Header style", + "numberedList": "Numbered list", + "bulletList": "Bullet list", + "checkedList": "Checked list", + "codeBlock": "Code block", + "quote": "Quote", + "increaseIndent": "Increase indent", + "decreaseIndent": "Decrease indent", + "insertURL": "Insert URL", + "visitLink": "Visit link", + "enterLink": "Enter link", + "enterMedia": "Enter media", + "edit": "Edit", + "apply": "Apply", + "findText": "Find text", + "moveToPreviousOccurrence": "Move to previous occurrence", + "moveToNextOccurrence": "Move to next occurrence", + "savedUsingTheNetwork": "Saved using the network", + "savedUsingLocalStorage": "Saved using the local storage", + "errorWhileSavingImage": "Error while saving image", + "pleaseEnterTextForYourLink": "e.g., 'Learn more'", + "pleaseEnterTheLinkURL": "e.g., 'https://example.com'", + "pleaseEnterAValidImageURL": "Please enter a valid image URL", + "hex": "Hex", + "material": "Material", + "color": "Color", + "pleaseEnterAValidVideoURL": "Por favor, ingrese una URL de video válida", + "photo": "Foto", + "image": "Imagen", + "caseSensitivityAndWholeWordSearch": "Sensibilidad a mayúsculas y minúsculas y búsqueda de palabras completas", + "insertImage": "Insertar imagen" + } + \ No newline at end of file diff --git a/lib/src/l10n/quill_fa.arb b/lib/src/l10n/quill_fa.arb new file mode 100644 index 00000000..bf470af5 --- /dev/null +++ b/lib/src/l10n/quill_fa.arb @@ -0,0 +1,82 @@ +{ + "@@locale": "fa", + "pasteLink": "جایگذاری لینک", + "ok": "تایید", + "selectColor": "انتخاب رنگ", + "gallery": "گالری", + "link": "لینک", + "open": "باز کردن", + "copy": "کپی", + "remove": "حذف", + "save": "ذخیره", + "zoom": "بزرگنمایی", + "saved": "ذخیره شد", + "text": "متن", + "resize": "تغییر اندازه", + "width": "عرض", + "height": "طول", + "size": "اندازه", + "small": "کوچک", + "large": "بزرگ", + "huge": "خیلی بزرگ", + "clear": "پاک کردن", + "font": "فونت", + "search": "جستجو", + "camera": "دوربین", + "video": "ویدیو", + "undo": "Undo", + "redo": "Redo", + "fontFamily": "Sخانواده فونت", + "fontSize": "اندازه فونت", + "bold": "توپر", + "subscript": "زیرنویس", + "superscript": "بالانویس", + "italic": "مورب", + "underline": "زیرخط", + "strikeThrough": "خط خورده", + "inlineCode": "کد درون خطی", + "fontColor": "رنگ فونت", + "backgroundColor": "رنگ زمینه", + "clearFormat": "پاکسازی فرمت", + "alignLeft": "چیدمان چپ", + "alignCenter": "چیدمان وسط", + "alignRight": "چیدمان راست", + "justifyWinWidth": "تضمین عرض پنجره", + "textDirection": "جهت متن", + "headerStyle": "سبک هدر", + "numberedList": "لیست شماره‌دار", + "bulletList": "لیست نقطه‌ای", + "checkedList": "لیست با علامت", + "codeBlock": "بلوک کد", + "quote": "نقل قول", + "increaseIndent": "افزایش تورفتگی", + "decreaseIndent": "کاهش تورفتگی", + "insertURL": "درج URL", + "visitLink": "بازدید از لینک", + "enterLink": "ورود لینک", + "enterMedia": "ورود رسانه", + "edit": "ویرایش", + "apply": "اعمال", + "findText": "جستجوی متن", + "moveToPreviousOccurrence": "انتقال به رخداد قبلی", + "moveToNextOccurrence": "انتقال به رخداد بعدی", + "savedUsingNetwork": "ذخیره شده با استفاده از شبکه", + "savedUsingLocalStorage": "ذخیره شده با استفاده از فضای ذخیره محلی", + "errorWhileSavingImage": "خطا در هنگام ذخیره تصویر", + "enterTextForYourLink": "برای مثال، 'بیشتر بیاموزید'", + "enterLinkURL": "برای مثال، 'https://example.com'", + "enterValidImageURL": "لطفاً یک URL تصویر معتبر وارد کنید", + "hex": "Hex", + "material": "مواد", + "color": "رنگ", + "savedUsingTheNetwork": "با استفاده از شبکه ذخیره شده است", + "pleaseEnterTextForYourLink": "لطفاً متن لینک خود را وارد کنید (مثال: 'بیشتر بدانید')", + "pleaseEnterTheLinkURL": "لطفاً URL لینک را وارد کنید (مثال: 'https://example.com')", + "pleaseEnterAValidImageURL": "لطفاً یک URL تصویر معتبر وارد کنید", + "pleaseEnterAValidVideoURL": "لطفاً یک URL ویدیوی معتبر وارد کنید", + "photo": "عکس", + "image": "تصویر", + "caseSensitivityAndWholeWordSearch": "حساسیت به کوچکی و بزرگی حروف و جستجوی کلمه کامل", + "insertImage": "وارد کردن تصویر" + } + \ No newline at end of file diff --git a/lib/src/l10n/quill_fr.arb b/lib/src/l10n/quill_fr.arb new file mode 100644 index 00000000..cbdbb2e0 --- /dev/null +++ b/lib/src/l10n/quill_fr.arb @@ -0,0 +1,78 @@ +{ + "@@locale": "fr", + "pasteLink": "Coller un lien", + "ok": "Ok", + "selectColor": "Choisir une couleur", + "gallery": "Galerie", + "link": "Lien", + "open": "Ouvrir", + "copy": "Copier", + "remove": "Supprimer", + "save": "Sauvegarder", + "zoom": "Zoomer", + "saved": "Enregistrée", + "text": "Texte", + "resize": "Redimensionner", + "width": "Largeur", + "height": "Hauteur", + "size": "Taille", + "small": "Petit", + "large": "Grand", + "huge": "Énorme", + "clear": "Supprimer la mise en forme", + "font": "Police", + "search": "Rechercher", + "camera": "Caméra", + "video": "Vidéo", + "undo": "Annuler", + "redo": "Refaire", + "fontFamily": "Famille de police", + "fontSize": "Taille de police", + "bold": "Gras", + "subscript": "Indice", + "superscript": "Exposant", + "italic": "Italique", + "underline": "Souligné", + "strikeThrough": "Barré", + "inlineCode": "Code en ligne", + "fontColor": "Couleur de police", + "backgroundColor": "Couleur de fond", + "clearFormat": "Effacer la mise en forme", + "alignLeft": "Aligner à gauche", + "alignCenter": "Aligner au centre", + "alignRight": "Aligner à droite", + "justifyWinWidth": "Justifier", + "textDirection": "Direction du texte", + "headerStyle": "Style d'en-tête", + "numberedList": "Liste numérotée", + "bulletList": "Liste à puces", + "checkedList": "Check-list", + "codeBlock": "Bloc de code", + "quote": "Citation", + "increaseIndent": "Augmenter le retrait", + "decreaseIndent": "Diminuer le retrait", + "insertURL": "Insérer une URL", + "visitLink": "Visiter", + "enterLink": "Entrer un lien", + "enterMedia": "Entrer un média", + "edit": "Modifier", + "apply": "Appliquer", + "findText": "Rechercher du texte", + "moveToPreviousOccurrence": "Aller à l'occurrence précédente", + "moveToNextOccurrence": "Aller à l'occurrence suivante", + "savedUsingTheNetwork": "Enregistré via le réseau", + "savedUsingLocalStorage": "Enregistré en utilisant le stockage local", + "errorWhileSavingImage": "Erreur lors de l'enregistrement de l'image", + "pleaseEnterTextForYourLink": "par exemple, 'En savoir plus'", + "pleaseEnterTheLinkURL": "par exemple, 'https://example.com'", + "pleaseEnterAValidImageURL": "Veuillez saisir une URL d'image valide", + "hex": "Hex", + "material": "Matériel", + "color": "Couleur", + "pleaseEnterAValidVideoURL": "Veuillez entrer une URL vidéo valide", + "photo": "Photo", + "image": "Image", + "caseSensitivityAndWholeWordSearch": "Sensibilité à la casse et recherche de mots entiers", + "insertImage": "Insérer une image" +} + \ No newline at end of file diff --git a/lib/src/l10n/quill_he.arb b/lib/src/l10n/quill_he.arb new file mode 100644 index 00000000..922495a4 --- /dev/null +++ b/lib/src/l10n/quill_he.arb @@ -0,0 +1,82 @@ +{ + "@@locale": "he", + "pasteLink": "הדבק את הלינק", + "ok": "אוקי", + "selectColor": "בחר צבע", + "gallery": "גלריה", + "link": "לינק", + "open": "פתח", + "copy": "העתק", + "remove": "מחק", + "save": "שמור", + "zoom": "זום", + "saved": "נשמר", + "text": "טקסט", + "resize": "שנה גודל", + "width": "רוחב", + "height": "גובה", + "size": "גודל", + "small": "קטן", + "large": "גדול", + "huge": "ענק", + "clear": "מחוק", + "font": "פונט", + "search": "חפש", + "camera": "מצלמה", + "video": "וידאו", + "undo": "בטל", + "redo": "בצע שוב", + "fontFamily": "משפחת הפונטים", + "fontSize": "גודל הפונט", + "bold": "מודגש", + "subscript": "כתוב בתחתית השורה", + "superscript": "כתוב בחלק העליון של השורה", + "italic": "נטוי", + "underline": "קו תחתון", + "strikeThrough": "קו חוצה", + "inlineCode": "קוד טקסט בתוך הטקסט", + "fontColor": "צבע טקסט", + "backgroundColor": "צבע רקע", + "clearFormat": "נקה פורמט", + "alignLeft": "יישור לשמאל", + "alignCenter": "יישור למרכז", + "alignRight": "יישור לימין", + "justifyWinWidth": "יישור לרוחב החלון", + "textDirection": "כיוון הטקסט", + "headerStyle": "סגנון הכותרת", + "numberedList": "רשימה ממוספרת", + "bulletList": "רשימה עם תבליטים", + "checkedList": "רשימת תיקולים", + "codeBlock": "בלוק קוד", + "quote": "ציטוט", + "increaseIndent": "הגדל את הזחות", + "decreaseIndent": "הקטן את הזחות", + "insertURL": "הוסף URL", + "visitLink": "בקר בלינק", + "enterLink": "הכנס לינק", + "enterMedia": "הכנס מדיה", + "edit": "ערוך", + "apply": "החל", + "findText": "מצא טקסט", + "moveToPreviousOccurrence": "התקדם להופעה הקודמת", + "moveToNextOccurrence": "התקדם להופעה הבאה", + "savedUsingNetwork": "נשמר באמצעות הרשת", + "savedUsingLocalStorage": "נשמר באמצעות אחסון מקומי", + "errorWhileSavingImage": "שגיאה בעת שמירת התמונה", + "enterTextForYourLink": "לדוגמה, 'מידע נוסף'", + "enterLinkURL": "לדוגמה, 'https://example.com'", + "enterValidImageURL": "אנא הכנס URL תמונה תקני", + "hex": "Hex", + "material": "חומר", + "color": "צבע", + "savedUsingTheNetwork": "נשמר באמצעות הרשת", + "pleaseEnterTextForYourLink": "אנא הזן טקסט לקישור שלך (לדוגמה, 'מידע נוסף')", + "pleaseEnterTheLinkURL": "אנא הזן את כתובת ה-URL של הקישור (לדוגמה, 'https://example.com')", + "pleaseEnterAValidImageURL": "אנא הזן כתובת URL תקינה של תמונה", + "pleaseEnterAValidVideoURL": "אנא הזן כתובת URL תקינה של וידיאו", + "photo": "תמונה", + "image": "תמונה", + "caseSensitivityAndWholeWordSearch": "רגישות לאותות רישיות וחיפוש לפי מילה שלמה", + "insertImage": "הכנס תמונה" + } + \ No newline at end of file diff --git a/lib/src/l10n/quill_hi.arb b/lib/src/l10n/quill_hi.arb new file mode 100644 index 00000000..9a8b1e4f --- /dev/null +++ b/lib/src/l10n/quill_hi.arb @@ -0,0 +1,82 @@ +{ + "@@locale": "hi", + "pasteLink": "लिंक पेस्ट करें", + "ok": "ठीक है", + "selectColor": "रंग चुनें", + "gallery": "गैलरी", + "link": "लिंक", + "open": "खोलें", + "copy": "कॉपी करें", + "remove": "हटाएं", + "save": "सुरक्षित करें", + "zoom": "बड़ा करें", + "saved": "सुरक्षित कर दिया गया है", + "text": "शब्द", + "resize": "आकार बदलें", + "width": "चौड़ाई", + "height": "ऊंचाई", + "size": "Size", + "small": "Small", + "large": "Large", + "huge": "Huge", + "clear": "Clear", + "font": "Font", + "search": "Search", + "camera": "Camera", + "video": "Video", + "undo": "Undo", + "redo": "Redo", + "fontFamily": "Sूची का नाम", + "fontSize": "फ़ॉन्ट का आकार", + "bold": "ताक़तवर", + "subscript": "अधोलेख", + "superscript": "अद्भुतलेख", + "italic": "तिरछा", + "underline": "रेखांकन", + "strikeThrough": "मार", + "inlineCode": "लाइन कोड", + "fontColor": "फॉन्ट का रंग", + "backgroundColor": "पृष्ठभूमि का रंग", + "clearFormat": "स्वच्छ स्वरूप", + "alignLeft": "बाएं संरेखित करें", + "alignCenter": "केंद्रित संरेखित करें", + "alignRight": "दाएं संरेखित करें", + "justifyWinWidth": "जस्टीफ़ी विन चौड़ाई", + "textDirection": "टेक्स्ट की दिशा", + "headerStyle": "हेडर शैली", + "numberedList": "संख्याबद्ध सूची", + "bulletList": "गोली दी गई सूची", + "checkedList": "जाँची गई सूची", + "codeBlock": "कोड ब्लॉक", + "quote": "नोट", + "increaseIndent": "इंडेंट बढ़ाएं", + "decreaseIndent": "इंडेंट कम करें", + "insertURL": "URL डालें", + "visitLink": "लिंक देखें", + "enterLink": "लिंक दर्ज करें", + "enterMedia": "मीडिया दर्ज करें", + "edit": "संपादित करें", + "apply": "लागू करें", + "findText": "मद को खोजें", + "moveToPreviousOccurrence": "पिछले घटनांतर पर जाएं", + "moveToNextOccurrence": "आगामी घटनांतर पर जाएं", + "savedUsingNetwork": "नेटवर्क का उपयोग करके सहेजा गया", + "savedUsingLocalStorage": "स्थानीय संग्रहण का उपयोग करके सहेजा गया", + "errorWhileSavingImage": "तस्वीर सहेजते समय त्रुटि", + "enterTextForYourLink": "उदाहरण के लिए, 'और जानें'", + "enterLinkURL": "उदाहरण के लिए, 'https://example.com'", + "enterValidImageURL": "कृपया एक मान्य छवि URL दर्ज करें", + "hex": "हेक्स", + "material": "सामग्री", + "color": "रंग", + "savedUsingTheNetwork": "नेटवर्क का उपयोग करके सहेजा गया", + "pleaseEnterTextForYourLink": "कृपया अपने लिंक के लिए एक पाठ दर्ज करें (उदाहरण: 'और अधिक जानें')", + "pleaseEnterTheLinkURL": "कृपया लिंक URL दर्ज करें (उदाहरण: 'https://example.com')", + "pleaseEnterAValidImageURL": "कृपया एक वैध चित्र URL दर्ज करें", + "pleaseEnterAValidVideoURL": "कृपया एक वैध वीडियो URL दर्ज करें", + "photo": "फोटो", + "image": "छवि", + "caseSensitivityAndWholeWordSearch": "केस सेंसिटिविटी और पूरे शब्द की खोज", + "insertImage": "छवि डालें" + } + \ No newline at end of file diff --git a/lib/src/l10n/quill_id.arb b/lib/src/l10n/quill_id.arb new file mode 100644 index 00000000..fbc87001 --- /dev/null +++ b/lib/src/l10n/quill_id.arb @@ -0,0 +1,82 @@ +{ + "@@locale": "id", + "pasteLink": "Tempel tautan", + "ok": "Oke", + "selectColor": "Pilih Warna", + "gallery": "Galeri", + "link": "Tautan", + "open": "Buka", + "copy": "Salin", + "remove": "Hapus", + "save": "Simpan", + "zoom": "Perbesar", + "saved": "Tersimpan", + "text": "Teks", + "resize": "Ubah Ukuran", + "width": "Lebar", + "height": "Tinggi", + "size": "Ukuran", + "small": "Kecil", + "large": "Besar", + "huge": "Sangat Besar", + "clear": "Hapus", + "font": "Font", + "search": "Cari", + "camera": "Kamera", + "video": "Video", + "undo": "Undo", + "redo": "Redo", + "fontFamily": "Keluarga Font", + "fontSize": "Ukuran Font", + "bold": "Tebal", + "subscript": "Subskrip", + "superscript": "Superskrip", + "italic": "Miring", + "underline": "Garis Bawah", + "strikeThrough": "Coret Saja", + "inlineCode": "Kode Inline", + "fontColor": "Warna Font", + "backgroundColor": "Warna Latar", + "clearFormat": "Hapus Format", + "alignLeft": "Rata Kiri", + "alignCenter": "Rata Tengah", + "alignRight": "Rata Kanan", + "justifyWinWidth": "Rata Kanan dan Kiri", + "textDirection": "Arah Teks", + "headerStyle": "Gaya Header", + "numberedList": "Daftar Bernomor", + "bulletList": "Daftar Poin", + "checkedList": "Daftar Dicentang", + "codeBlock": "Blok Kode", + "quote": "Kutipan", + "increaseIndent": "Tambah Indentasi", + "decreaseIndent": "Kurangi Indentasi", + "insertURL": "Masukkan URL", + "visitLink": "Kunjungi Tautan", + "enterLink": "Masukkan Tautan", + "enterMedia": "Masukkan Media", + "edit": "Edit", + "apply": "Terapkan", + "findText": "Temukan Teks", + "moveToPreviousOccurrence": "Pindah ke Kejadian Sebelumnya", + "moveToNextOccurrence": "Pindah ke Kejadian Berikutnya", + "savedUsingNetwork": "Tersimpan menggunakan jaringan", + "savedUsingLocalStorage": "Tersimpan menggunakan penyimpanan lokal", + "errorWhileSavingImage": "Error saat menyimpan gambar", + "enterTextForYourLink": "contoh: 'Pelajari lebih lanjut'", + "enterLinkURL": "contoh: 'https://example.com'", + "enterValidImageURL": "Silakan masukkan URL gambar yang valid", + "hex": "Hex", + "material": "Material", + "color": "Warna", + "savedUsingTheNetwork": "Tersimpan menggunakan jaringan", + "pleaseEnterTextForYourLink": "Harap masukkan teks untuk tautan Anda (contoh: 'Pelajari lebih lanjut')", + "pleaseEnterTheLinkURL": "Harap masukkan URL tautan (contoh: 'https://example.com')", + "pleaseEnterAValidImageURL": "Harap masukkan URL gambar yang valid", + "pleaseEnterAValidVideoURL": "Harap masukkan URL video yang valid", + "photo": "Foto", + "image": "Gambar", + "caseSensitivityAndWholeWordSearch": "Sensitivitas huruf besar dan kecil dan pencarian kata utuh", + "insertImage": "Sisipkan Gambar" + } + \ No newline at end of file diff --git a/lib/src/l10n/quill_it.arb b/lib/src/l10n/quill_it.arb new file mode 100644 index 00000000..75eb9a3c --- /dev/null +++ b/lib/src/l10n/quill_it.arb @@ -0,0 +1,82 @@ +{ + "@@locale": "it", + "pasteLink": "Incolla un collegamento", + "ok": "Ok", + "selectColor": "Seleziona Colore", + "gallery": "Galleria", + "link": "Collegamento", + "open": "Apri", + "copy": "Copia", + "remove": "Rimuovi", + "save": "Salva", + "zoom": "Ingrandisci", + "saved": "Salvato", + "text": "Testo", + "resize": "Ridimensiona", + "width": "Larghezza", + "height": "Altezza", + "size": "Dimensione", + "small": "Piccolo", + "large": "Largo", + "huge": "Enorme", + "clear": "Cancella", + "font": "Font", + "search": "Ricerca", + "camera": "Camera", + "video": "Video", + "undo": "Annulla", + "redo": "Ripeti", + "fontFamily": "Famiglia del carattere", + "fontSize": "Dimensione del carattere", + "bold": "Grassetto", + "subscript": "Pedice", + "superscript": "Apice", + "italic": "Corsivo", + "underline": "Sottolineato", + "strikeThrough": "Barrato", + "inlineCode": "Codice inline", + "fontColor": "Colore del carattere", + "backgroundColor": "Colore di sfondo", + "clearFormat": "Cancella formato", + "alignLeft": "Allinea a sinistra", + "alignCenter": "Allinea al centro", + "alignRight": "Allinea a destra", + "justifyWinWidth": "Giustifica per larghezza finestra", + "textDirection": "Direzione testo", + "headerStyle": "Stile intestazione", + "numberedList": "Elenco numerato", + "bulletList": "Elenco puntato", + "checkedList": "Elenco con segni di spunta", + "codeBlock": "Blocco di codice", + "quote": "Citazione", + "increaseIndent": "Aumenta rientro", + "decreaseIndent": "Diminuisci rientro", + "insertURL": "Inserisci URL", + "visitLink": "Visita il collegamento", + "enterLink": "Inserisci il collegamento", + "enterMedia": "Inserisci multimedia", + "edit": "Modifica", + "apply": "Applica", + "hex": "Esadecimale", + "material": "Materiale", + "color": "Colore", + "findText": "Trova testo", + "moveToPreviousOccurrence": "Vai all'occorrenza precedente", + "moveToNextOccurrence": "Vai all'occorrenza successiva", + "savedUsingNetwork": "Salvato utilizzando la rete", + "savedUsingLocalStorage": "Salvato utilizzando la memorizzazione locale", + "errorWhileSavingImage": "Errore durante il salvataggio dell'immagine", + "enterTextForYourLink": "es. 'Per saperne di più'", + "enterLinkURL": "es. 'https://example.com'", + "enterValidImageURL": "Inserisci un URL di immagine valido", + "savedUsingTheNetwork": "Salvato utilizzando la rete", + "pleaseEnterTextForYourLink": "Inserisci un testo per il tuo link (ad esempio, 'Per saperne di più')", + "pleaseEnterTheLinkURL": "Inserisci l'URL del link (ad esempio, 'https://example.com')", + "pleaseEnterAValidImageURL": "Inserisci un URL di immagine valido", + "pleaseEnterAValidVideoURL": "Inserisci un URL video valido", + "photo": "Foto", + "image": "Immagine", + "caseSensitivityAndWholeWordSearch": "Sensibilità maiuscole/minuscole e ricerca di parole intere", + "insertImage": "Inserisci immagine" + } + \ No newline at end of file diff --git a/lib/src/l10n/quill_ja.arb b/lib/src/l10n/quill_ja.arb new file mode 100644 index 00000000..3827102f --- /dev/null +++ b/lib/src/l10n/quill_ja.arb @@ -0,0 +1,78 @@ +{ + "@@locale": "ja", + "pasteLink": "リンクをペースト", + "ok": "完了", + "selectColor": "色を選択", + "gallery": "写真集", + "link": "リンク", + "open": "開く", + "copy": "コピー", + "remove": "削除", + "save": "保存", + "zoom": "拡大", + "saved": "保存済み", + "text": "文字", + "resize": "サイズを調整", + "width": "幅", + "height": "高さ", + "size": "サイズ", + "small": "小さい", + "large": "大きい", + "huge": "でっかい", + "clear": "クリア", + "font": "フォント", + "search": "検索", + "camera": "カメラ", + "video": "ビデオ", + "undo": "取り消し", + "redo": "やり直し", + "fontFamily": "フォントファミリー", + "fontSize": "フォントサイズ", + "bold": "太字", + "subscript": "下付き", + "superscript": "上付き", + "italic": "斜体", + "underline": "下線", + "strikeThrough": "取り消し線", + "inlineCode": "インラインコード", + "fontColor": "フォントカラー", + "backgroundColor": "ベースカラー", + "clearFormat": "クリアフォーマット", + "alignLeft": "左揃え", + "alignCenter": "センターアライメント", + "alignRight": "右揃え", + "justifyWinWidth": "両端揃え", + "textDirection": "文字方向", + "headerStyle": "タイトルスタイル", + "numberedList": "順序付きリスト", + "bulletList": "順序無しリスト", + "checkedList": "チェックボックス", + "codeBlock": "コード", + "quote": "引用", + "increaseIndent": "インデントを増やす", + "decreaseIndent": "インデントを減らす", + "insertURL": "ハイパーリンクを挿入", + "visitLink": "ハイパーリンクを訪問", + "enterLink": "ハイパーリンクを輸入", + "enterMedia": "ミディアムを輸入", + "edit": "編集", + "apply": "応用", + "findText": "検索テキスト", + "moveToPreviousOccurrence": "前のマッチ", + "moveToNextOccurrence": "次のマッチ", + "savedUsingTheNetwork": "ネットワークを使用して保存", + "savedUsingLocalStorage": "ローカルストレージを使用して保存", + "errorWhileSavingImage": "画像の保存中にエラーが発生しました", + "pleaseEnterTextForYourLink": "例: 'Learn more'", + "pleaseEnterTheLinkURL": "例: 'https://example.com'", + "pleaseEnterAValidImageURL": "有効な画像URLを入力してください", + "hex": "Hex", + "material": "Material", + "color": "Color", + "pleaseEnterAValidVideoURL": "有効なビデオURLを入力してください", + "photo": "写真", + "image": "画像", + "caseSensitivityAndWholeWordSearch": "大文字と小文字の区別と完全一致検索", + "insertImage": "画像を挿入" + } + \ No newline at end of file diff --git a/lib/src/l10n/quill_ko.arb b/lib/src/l10n/quill_ko.arb new file mode 100644 index 00000000..87608dae --- /dev/null +++ b/lib/src/l10n/quill_ko.arb @@ -0,0 +1,78 @@ +{ + "@@locale": "ko", + "pasteLink": "링크를 붙여넣어 주세요.", + "ok": "확인", + "selectColor": "색상 선택", + "gallery": "갤러리", + "link": "링크", + "open": "열기", + "copy": "복사하기", + "remove": "제거하기", + "save": "저장하기", + "zoom": "확대하기", + "saved": "저장되었습니다.", + "text": "텍스트", + "resize": "크기조정", + "width": "넓이", + "height": "높이", + "size": "크기", + "small": "작게", + "large": "크게", + "huge": "매우크게", + "clear": "초기화", + "font": "글꼴", + "search": "검색", + "camera": "카메라", + "video": "비디오", + "undo": "Undo", + "redo": "Redo", + "fontFamily": "Font family", + "fontSize": "Font size", + "bold": "Bold", + "subscript": "Subscript", + "superscript": "Superscript", + "italic": "Italic", + "underline": "Underline", + "strikeThrough": "Strike through", + "inlineCode": "Inline code", + "fontColor": "Font color", + "backgroundColor": "Background color", + "clearFormat": "Clear format", + "alignLeft": "Align left", + "alignCenter": "Align center", + "alignRight": "Align right", + "justifyWinWidth": "Justify win width", + "textDirection": "Text direction", + "headerStyle": "Header style", + "numberedList": "Numbered list", + "bulletList": "Bullet list", + "checkedList": "Checked list", + "codeBlock": "Code block", + "quote": "Quote", + "increaseIndent": "Increase indent", + "decreaseIndent": "Decrease indent", + "insertURL": "Insert URL", + "visitLink": "Visit link", + "enterLink": "Enter link", + "enterMedia": "Enter media", + "edit": "Edit", + "apply": "Apply", + "findText": "Find text", + "moveToPreviousOccurrence": "Move to previous occurrence", + "moveToNextOccurrence": "Move to next occurrence", + "savedUsingTheNetwork": "Saved using the network", + "savedUsingLocalStorage": "Saved using the local storage", + "errorWhileSavingImage": "Error while saving image", + "pleaseEnterTextForYourLink": "e.g., 'Learn more'", + "pleaseEnterTheLinkURL": "e.g., 'https://example.com'", + "pleaseEnterAValidImageURL": "Please enter a valid image URL", + "hex": "Hex", + "material": "Material", + "color": "Color", + "pleaseEnterAValidVideoURL": "유효한 비디오 URL을 입력하세요", + "photo": "사진", + "image": "이미지", + "caseSensitivityAndWholeWordSearch": "대소문자 구분 및 전체 단어 검색", + "insertImage": "이미지 삽입" + } + \ No newline at end of file diff --git a/lib/src/l10n/quill_ms.arb b/lib/src/l10n/quill_ms.arb new file mode 100644 index 00000000..6dd29254 --- /dev/null +++ b/lib/src/l10n/quill_ms.arb @@ -0,0 +1,82 @@ +{ + "@@locale": "ms", + "pasteLink": "Tampal Pautan", + "ok": "Ok", + "selectColor": "Pilih Warna", + "gallery": "Galeri", + "link": "Pautan", + "open": "Buka", + "copy": "Salin", + "remove": "Buang", + "save": "Simpan", + "zoom": "Zum", + "saved": "Telah Disimpan", + "text": "Perkataan", + "resize": "Ubah saiz", + "width": "Lebar", + "height": "Tinggi", + "size": "Saiz", + "small": "Kecil", + "large": "Besar", + "huge": "Amat Besar", + "clear": "Padam", + "font": "Fon", + "search": "Carian", + "camera": "Kamera", + "video": "Video", + "undo": "Undo", + "redo": "Redo", + "fontFamily": "Font family", + "fontSize": "Font size", + "bold": "Bold", + "subscript": "Subscript", + "superscript": "Superscript", + "italic": "Italic", + "underline": "Underline", + "strikeThrough": "Strike through", + "inlineCode": "Inline code", + "fontColor": "Font color", + "backgroundColor": "Background color", + "clearFormat": "Clear format", + "alignLeft": "Align left", + "alignCenter": "Align center", + "alignRight": "Align right", + "justifyWinWidth": "Justify win width", + "textDirection": "Text direction", + "headerStyle": "Header style", + "numberedList": "Numbered list", + "bulletList": "Bullet list", + "checkedList": "Checked list", + "codeBlock": "Code block", + "quote": "Quote", + "increaseIndent": "Increase indent", + "decreaseIndent": "Decrease indent", + "insertURL": "Insert URL", + "visitLink": "Visit link", + "enterLink": "Enter link", + "enterMedia": "Enter media", + "edit": "Edit", + "apply": "Apply", + "findText": "Find text", + "moveToPreviousOccurrence": "Move to previous occurrence", + "moveToNextOccurrence": "Move to next occurrence", + "savedUsingNetwork": "Saved using the network", + "savedUsingLocalStorage": "Saved using the local storage", + "errorWhileSavingImage": "Error while saving image", + "enterTextForYourLink": "e.g., 'Learn more'", + "enterLinkURL": "e.g., 'https://example.com'", + "enterValidImageURL": "Please enter a valid image URL", + "hex": "Hex", + "material": "Bahan", + "color": "Warna", + "savedUsingTheNetwork": "Disimpan menggunakan rangkaian", + "pleaseEnterTextForYourLink": "Sila masukkan teks untuk pautan anda (contoh, 'Ketahui lebih lanjut')", + "pleaseEnterTheLinkURL": "Sila masukkan URL pautan (contoh, 'https://example.com')", + "pleaseEnterAValidImageURL": "Sila masukkan URL imej yang sah", + "pleaseEnterAValidVideoURL": "Sila masukkan URL video yang sah", + "photo": "Foto", + "image": "Imej", + "caseSensitivityAndWholeWordSearch": "Sensitiviti huruf besar dan kecil dan carian penuh perkataan", + "insertImage": "Masukkan imej" +} + \ No newline at end of file diff --git a/lib/src/l10n/quill_nl.arb b/lib/src/l10n/quill_nl.arb new file mode 100644 index 00000000..66a9c572 --- /dev/null +++ b/lib/src/l10n/quill_nl.arb @@ -0,0 +1,82 @@ +{ + "@@locale": "nl", + "pasteLink": "Plak een link", + "ok": "Ok", + "selectColor": "Selecteer kleur", + "gallery": "Gallerij", + "link": "Link", + "open": "Open", + "copy": "Kopieer", + "remove": "Verwijderd", + "save": "Opslaan", + "zoom": "Zoom", + "saved": "Opgeslagen", + "text": "Tekst", + "resize": "Formaat wijzigen", + "width": "Breedte", + "height": "Hoogte", + "size": "Grootte", + "small": "Small", + "large": "Large", + "huge": "Huge", + "clear": "Clear", + "font": "Font", + "search": "Search", + "camera": "Camera", + "video": "Video", + "undo": "Undo", + "redo": "Redo", + "fontFamily": "Font family", + "fontSize": "Font size", + "bold": "Bold", + "subscript": "Subscript", + "superscript": "Superscript", + "italic": "Italic", + "underline": "Underline", + "strikeThrough": "Strike through", + "inlineCode": "Inline code", + "fontColor": "Font color", + "backgroundColor": "Background color", + "clearFormat": "Clear format", + "alignLeft": "Align left", + "alignCenter": "Align center", + "alignRight": "Align right", + "justifyWinWidth": "Justify win width", + "textDirection": "Text direction", + "headerStyle": "Header style", + "numberedList": "Numbered list", + "bulletList": "Bullet list", + "checkedList": "Checked list", + "codeBlock": "Code block", + "quote": "Quote", + "increaseIndent": "Increase indent", + "decreaseIndent": "Decrease indent", + "insertURL": "Insert URL", + "visitLink": "Visit link", + "enterLink": "Enter link", + "enterMedia": "Enter media", + "edit": "Edit", + "apply": "Apply", + "findText": "Find text", + "moveToPreviousOccurrence": "Move to previous occurrence", + "moveToNextOccurrence": "Move to next occurrence", + "savedUsingNetwork": "Saved using the network", + "savedUsingLocalStorage": "Saved using the local storage", + "errorWhileSavingImage": "Error while saving image", + "enterTextForYourLink": "Bijvoorbeeld, 'Lees meer'", + "enterLinkURL": "Bijvoorbeeld, 'https://example.com'", + "enterValidImageURL": "Voer een geldige afbeeldings-URL in", + "hex": "Hex", + "material": "Materiaal", + "color": "Kleur", + "savedUsingTheNetwork": "Opgeslagen via het netwerk", + "pleaseEnterTextForYourLink": "Voer tekst in voor uw link (bijvoorbeeld 'Meer weten')", + "pleaseEnterTheLinkURL": "Voer de URL van de link in (bijvoorbeeld 'https://example.com')", + "pleaseEnterAValidImageURL": "Voer een geldige URL voor de afbeelding in", + "pleaseEnterAValidVideoURL": "Voer een geldige URL voor de video in", + "photo": "Foto", + "image": "Afbeelding", + "caseSensitivityAndWholeWordSearch": "Hoofdlettergevoeligheid en volledig woord zoeken", + "insertImage": "Afbeelding invoegen" + } + \ No newline at end of file diff --git a/lib/src/l10n/quill_no.arb b/lib/src/l10n/quill_no.arb new file mode 100644 index 00000000..13d63bb7 --- /dev/null +++ b/lib/src/l10n/quill_no.arb @@ -0,0 +1,82 @@ +{ + "@@locale": "no", + "pasteLink": "Lim inn lenke", + "ok": "Ok", + "selectColor": "Velg farge", + "gallery": "Galleri", + "link": "Lenke", + "open": "Åpne", + "copy": "Kopier", + "remove": "Fjern", + "save": "Lagre", + "zoom": "Zoom", + "saved": "Lagret", + "text": "Tekst", + "resize": "Endre størrelse", + "width": "Bredde", + "height": "Høyde", + "size": "Størrelse", + "small": "Liten", + "large": "Stor", + "huge": "Enorm", + "clear": "Fjern", + "font": "Skrifttype", + "search": "Søk", + "camera": "Kamera", + "video": "Video", + "undo": "Angre", + "redo": "Gjør om", + "fontFamily": "Skriftfamilie", + "fontSize": "Skriftstørrelse", + "bold": "Fet", + "subscript": "Senket skrift", + "superscript": "Hevet skrift", + "italic": "Kursiv", + "underline": "Understreket", + "strikeThrough": "Gjennomstreking", + "inlineCode": "In-line kode", + "fontColor": "Skriftfarge", + "backgroundColor": "Bakgrunnsfarge", + "clearFormat": "Fjern formatering", + "alignLeft": "Venstrejuster", + "alignCenter": "Sentrer", + "alignRight": "Høyrejuster", + "justifyWinWidth": "Rettferdiggjør bredden", + "textDirection": "Tekstretning", + "headerStyle": "Overskriftsstil", + "numberedList": "Nummerert liste", + "bulletList": "Punktliste", + "checkedList": "Avkrysset liste", + "codeBlock": "Kodeblokk", + "quote": "Sitert tekst", + "increaseIndent": "Øk innrykk", + "decreaseIndent": "Mink innrykk", + "insertURL": "Sett inn URL", + "visitLink": "Besøk lenken", + "enterLink": "Skriv inn lenken", + "enterMedia": "Sett inn media", + "edit": "Rediger", + "apply": "Bruk", + "findText": "Finn tekst", + "moveToPreviousOccurrence": "Gå til forrige forekomst", + "moveToNextOccurrence": "Gå til neste forekomst", + "savedUsingNetwork": "Lagret ved hjelp av nettverket", + "savedUsingLocalStorage": "Lagret ved hjelp av lokal lagring", + "errorWhileSavingImage": "Feil ved lagring av bilde", + "enterTextForYourLink": "f.eks. 'Lær mer'", + "enterLinkURL": "f.eks. 'https://example.com'", + "enterValidImageURL": "Vennligst skriv inn en gyldig bilde-URL", + "hex": "Hex", + "material": "Materiale", + "color": "Farge", + "savedUsingTheNetwork": "Lagret ved hjelp av nettverket", + "pleaseEnterTextForYourLink": "Vennligst skriv inn tekst for lenken din (for eksempel 'Lær mer')", + "pleaseEnterTheLinkURL": "Vennligst skriv inn lenkens URL (for eksempel 'https://example.com')", + "pleaseEnterAValidImageURL": "Vennligst skriv inn en gyldig bilde-URL", + "pleaseEnterAValidVideoURL": "Vennligst skriv inn en gyldig video-URL", + "photo": "Bilde", + "image": "Bilde", + "caseSensitivityAndWholeWordSearch": "Stor/liten bokstavfølsomhet og helordsøk", + "insertImage": "Sett inn bilde" + } + \ No newline at end of file diff --git a/lib/src/l10n/quill_pl.arb b/lib/src/l10n/quill_pl.arb new file mode 100644 index 00000000..e165e81d --- /dev/null +++ b/lib/src/l10n/quill_pl.arb @@ -0,0 +1,78 @@ +{ + "@@locale": "pl", + "pasteLink": "Wklej link", + "ok": "OK", + "selectColor": "Wybierz kolor", + "gallery": "Galeria", + "link": "Link", + "open": "Otwórz", + "copy": "Kopiuj", + "remove": "Usuń", + "save": "Zapisz", + "zoom": "Powiększenie", + "saved": "Zapisano", + "text": "Tekst", + "resize": "Resize", + "width": "Width", + "height": "Height", + "size": "Size", + "small": "Small", + "large": "Large", + "huge": "Huge", + "clear": "Clear", + "font": "Font", + "search": "Search", + "camera": "Camera", + "video": "Video", + "undo": "Undo", + "redo": "Redo", + "fontFamily": "Font family", + "fontSize": "Font size", + "bold": "Bold", + "subscript": "Subscript", + "superscript": "Superscript", + "italic": "Italic", + "underline": "Underline", + "strikeThrough": "Strike through", + "inlineCode": "Inline code", + "fontColor": "Font color", + "backgroundColor": "Background color", + "clearFormat": "Clear format", + "alignLeft": "Align left", + "alignCenter": "Align center", + "alignRight": "Align right", + "justifyWinWidth": "Justify win width", + "textDirection": "Text direction", + "headerStyle": "Header style", + "numberedList": "Numbered list", + "bulletList": "Bullet list", + "checkedList": "Checked list", + "codeBlock": "Code block", + "quote": "Quote", + "increaseIndent": "Increase indent", + "decreaseIndent": "Decrease indent", + "insertURL": "Insert URL", + "visitLink": "Visit link", + "enterLink": "Enter link", + "enterMedia": "Enter media", + "edit": "Edit", + "apply": "Apply", + "findText": "Find text", + "moveToPreviousOccurrence": "Move to previous occurrence", + "moveToNextOccurrence": "Move to next occurrence", + "savedUsingTheNetwork": "Saved using the network", + "savedUsingLocalStorage": "Saved using the local storage", + "errorWhileSavingImage": "Error while saving image", + "pleaseEnterTextForYourLink": "e.g., 'Learn more'", + "pleaseEnterTheLinkURL": "e.g., 'https://example.com'", + "pleaseEnterAValidImageURL": "Please enter a valid image URL", + "hex": "Hex", + "material": "Materiał", + "color": "Kolor", + "pleaseEnterAValidVideoURL": "Proszę wprowadzić poprawny adres URL wideo", + "photo": "Zdjęcie", + "image": "Obraz", + "caseSensitivityAndWholeWordSearch": "Czułość na wielkość liter i wyszukiwanie całego słowa", + "insertImage": "Wstaw obraz" + } + \ No newline at end of file diff --git a/lib/src/l10n/quill_pt.arb b/lib/src/l10n/quill_pt.arb new file mode 100644 index 00000000..3e84c032 --- /dev/null +++ b/lib/src/l10n/quill_pt.arb @@ -0,0 +1,78 @@ +{ + "@@locale": "pt", + "pasteLink": "Colar um link", + "ok": "Ok", + "selectColor": "Selecionar uma cor", + "gallery": "Galeria", + "link": "Link", + "open": "Abra", + "copy": "Copiar", + "remove": "Remover", + "save": "Salvar", + "zoom": "Zoom", + "saved": "Salvo", + "text": "Texto", + "resize": "Redimencionar", + "width": "Largura", + "height": "Altura", + "size": "Tamanho", + "small": "Pequeno", + "large": "Grande", + "huge": "Gigante", + "clear": "Limpar", + "font": "Fonte", + "search": "Search", + "camera": "Camera", + "video": "Video", + "undo": "Undo", + "redo": "Redo", + "fontFamily": "Font family", + "fontSize": "Font size", + "bold": "Bold", + "subscript": "Subscript", + "superscript": "Superscript", + "italic": "Italic", + "underline": "Underline", + "strikeThrough": "Strike through", + "inlineCode": "Inline code", + "fontColor": "Font color", + "backgroundColor": "Background color", + "clearFormat": "Clear format", + "alignLeft": "Align left", + "alignCenter": "Align center", + "alignRight": "Align right", + "justifyWinWidth": "Justify win width", + "textDirection": "Text direction", + "headerStyle": "Header style", + "numberedList": "Numbered list", + "bulletList": "Bullet list", + "checkedList": "Checked list", + "codeBlock": "Code block", + "quote": "Quote", + "increaseIndent": "Increase indent", + "decreaseIndent": "Decrease indent", + "insertURL": "Insert URL", + "visitLink": "Visit link", + "enterLink": "Enter link", + "enterMedia": "Enter media", + "edit": "Edit", + "apply": "Apply", + "findText": "Find text", + "moveToPreviousOccurrence": "Move to previous occurrence", + "moveToNextOccurrence": "Move to next occurrence", + "savedUsingTheNetwork": "Guardado através da network", + "savedUsingLocalStorage": "Guardado através do armazenamento local", + "errorWhileSavingImage": "Erro a gravar imagem", + "pleaseEnterTextForYourLink": "e.g., 'Learn more'", + "pleaseEnterTheLinkURL": "e.g., 'https://example.com'", + "pleaseEnterAValidImageURL": "Please enter a valid image URL", + "hex": "Hex", + "material": "Material", + "color": "Cor", + "pleaseEnterAValidVideoURL": "Por favor, insira uma URL de vídeo válida", + "photo": "Foto", + "image": "Imagem", + "caseSensitivityAndWholeWordSearch": "Sensibilidade a maiúsculas e minúsculas e pesquisa de palavras inteiras", + "insertImage": "Inserir imagem" +} + \ No newline at end of file diff --git a/lib/src/l10n/quill_pt_br.arb b/lib/src/l10n/quill_pt_br.arb new file mode 100644 index 00000000..627667f1 --- /dev/null +++ b/lib/src/l10n/quill_pt_br.arb @@ -0,0 +1,78 @@ +{ + "@@locale": "pt_BR", + "pasteLink": "Colar um link", + "ok": "Ok", + "selectColor": "Selecionar uma cor", + "gallery": "Galeria", + "link": "Link", + "open": "Abrir", + "copy": "Copiar", + "remove": "Remover", + "save": "Salvar", + "zoom": "Zoom", + "saved": "Salvo", + "text": "Texto", + "resize": "Redimensionar", + "width": "Largura", + "height": "Altura", + "size": "Tamanho", + "small": "Pequeno", + "large": "Grande", + "huge": "Gigante", + "clear": "Limpar", + "font": "Fonte", + "search": "Buscar", + "camera": "Câmera", + "video": "Vídeo", + "undo": "Desfazer", + "redo": "Refazer", + "fontFamily": "Fonte", + "fontSize": "Tamanho da fonte", + "bold": "Negrito", + "subscript": "Subscrito", + "superscript": "Sobrescrito", + "italic": "Itálico", + "underline": "Sublinhado", + "strikeThrough": "Tachado", + "inlineCode": "Inline code", + "fontColor": "Cor da fonte", + "backgroundColor": "Cor do fundo", + "clearFormat": "Limpar formatação", + "alignLeft": "Texto à esquerda", + "alignCenter": "Centralizar", + "alignRight": "Texto à direita", + "justifyWinWidth": "Justificado", + "textDirection": "Direção do texto", + "headerStyle": "Estilo de cabeçalho", + "numberedList": "Numeração", + "bulletList": "Marcadores", + "checkedList": "Lista de verificação", + "codeBlock": "Code block", + "quote": "Citação", + "increaseIndent": "Aumentar recuo", + "decreaseIndent": "Diminuir recuo", + "insertURL": "Inserir URL", + "visitLink": "Visitar link", + "enterLink": "Inserir link", + "enterMedia": "Inserir mídia", + "edit": "Editar", + "apply": "Aplicar", + "findText": "Find text", + "moveToPreviousOccurrence": "Move to previous occurrence", + "moveToNextOccurrence": "Move to next occurrence", + "savedUsingTheNetwork": "Saved using the network", + "savedUsingLocalStorage": "Saved using the local storage", + "errorWhileSavingImage": "Error while saving image", + "pleaseEnterTextForYourLink": "e.g., 'Learn more'", + "pleaseEnterTheLinkURL": "e.g., 'https://example.com'", + "pleaseEnterAValidImageURL": "Please enter a valid image URL", + "hex": "Hex", + "material": "Material", + "color": "Cor", + "pleaseEnterAValidVideoURL": "Por favor, insira uma URL de vídeo válida", + "photo": "Foto", + "image": "Imagem", + "caseSensitivityAndWholeWordSearch": "Sensibilidade a maiúsculas e minúsculas e pesquisa de palavras inteiras", + "insertImage": "Inserir imagem" + } + \ No newline at end of file diff --git a/lib/src/l10n/quill_ru.arb b/lib/src/l10n/quill_ru.arb new file mode 100644 index 00000000..20e035c7 --- /dev/null +++ b/lib/src/l10n/quill_ru.arb @@ -0,0 +1,78 @@ +{ + "@@locale": "ru", + "pasteLink": "Вставить ссылку", + "ok": "ОК", + "selectColor": "Выбрать цвет", + "gallery": "Галерея", + "link": "Ссылка", + "open": "Открыть", + "copy": "Копировать", + "remove": "Удалить", + "save": "Сохранить", + "zoom": "Увеличить", + "saved": "Сохранено", + "text": "Текст", + "resize": "Resize", + "width": "Width", + "height": "Height", + "size": "Size", + "small": "Small", + "large": "Large", + "huge": "Huge", + "clear": "Clear", + "font": "Font", + "search": "Search", + "camera": "Camera", + "video": "Video", + "undo": "Undo", + "redo": "Redo", + "fontFamily": "Font family", + "fontSize": "Font size", + "bold": "Bold", + "subscript": "Subscript", + "superscript": "Superscript", + "italic": "Italic", + "underline": "Underline", + "strikeThrough": "Strike through", + "inlineCode": "Inline code", + "fontColor": "Font color", + "backgroundColor": "Background color", + "clearFormat": "Clear format", + "alignLeft": "Align left", + "alignCenter": "Align center", + "alignRight": "Align right", + "justifyWinWidth": "Justify win width", + "textDirection": "Text direction", + "headerStyle": "Header style", + "numberedList": "Numbered list", + "bulletList": "Bullet list", + "checkedList": "Checked list", + "codeBlock": "Code block", + "quote": "Quote", + "increaseIndent": "Increase indent", + "decreaseIndent": "Decrease indent", + "insertURL": "Insert URL", + "visitLink": "Visit link", + "enterLink": "Enter link", + "enterMedia": "Enter media", + "edit": "Edit", + "apply": "Apply", + "findText": "Find text", + "moveToPreviousOccurrence": "Move to previous occurrence", + "moveToNextOccurrence": "Move to next occurrence", + "savedUsingTheNetwork": "Saved using the network", + "savedUsingLocalStorage": "Saved using the local storage", + "errorWhileSavingImage": "Error while saving image", + "pleaseEnterTextForYourLink": "e.g., 'Learn more'", + "pleaseEnterTheLinkURL": "e.g., 'https://example.com'", + "pleaseEnterAValidImageURL": "Please enter a valid image URL", + "hex": "Hex", + "material": "Материал", + "color": "Цвет", + "pleaseEnterAValidVideoURL": "Пожалуйста, введите действительный URL-адрес видео", + "photo": "Фото", + "image": "Изображение", + "caseSensitivityAndWholeWordSearch": "Чувствительность к регистру и поиск целых слов", + "insertImage": "Вставить изображение" +} + \ No newline at end of file diff --git a/lib/src/l10n/quill_sr.arb b/lib/src/l10n/quill_sr.arb new file mode 100644 index 00000000..8070e920 --- /dev/null +++ b/lib/src/l10n/quill_sr.arb @@ -0,0 +1,82 @@ +{ + "@@locale": "sr", + "pasteLink": "Nalepi vezu", + "ok": "OK", + "selectColor": "Odaberi boju", + "gallery": "Galerija", + "link": "Veza", + "open": "Otvori", + "copy": "Kopiraj", + "remove": "Ukloni", + "save": "Sačuvaj", + "zoom": "Uvećaj", + "saved": "Sačuvano", + "text": "Tekst", + "resize": "Promeni veličinu", + "width": "Širina", + "height": "Visina", + "size": "Veličina", + "small": "Malo", + "large": "Veliko", + "huge": "Ogromno", + "clear": "Obriši", + "font": "Font", + "search": "Pretraga", + "camera": "Kamera", + "video": "Video", + "undo": "Poništi", + "redo": "Ponovo", + "fontFamily": "Porodica fonta", + "fontSize": "Veličina fonta", + "bold": "Podebljano", + "subscript": "Indeks", + "superscript": "Stepen", + "italic": "Iskošeno", + "underline": "Podvučeno", + "strikeThrough": "Precrtano", + "inlineCode": "Ugrađeni kôd", + "fontColor": "Boja fonta", + "backgroundColor": "Boja pozadine", + "clearFormat": "Obriši format", + "alignLeft": "Poravnanje levo", + "alignCenter": "Poravnanje centar", + "alignRight": "Poravnanje desno", + "justifyWinWidth": "Centriraj širinu prozora", + "textDirection": "Smer teksta", + "headerStyle": "Stil zaglavlja", + "numberedList": "Numerisana lista", + "bulletList": "Lista sa znakovima", + "checkedList": "Proverena lista", + "codeBlock": "Blok koda", + "quote": "Citat", + "increaseIndent": "Povećaj uvlačenje", + "decreaseIndent": "Smanji uvlačenje", + "insertURL": "Ubaci URL", + "visitLink": "Poseti link", + "enterLink": "Unesi link", + "enterMedia": "Unesi medij", + "edit": "Uredi", + "apply": "Primeni", + "findText": "Nađi tekst", + "moveToPreviousOccurrence": "Idi na prethodno pojavljivanje", + "moveToNextOccurrence": "Idi na sledeće pojavljivanje", + "savedUsingNetwork": "Sačuvano korišćenjem mreže", + "savedUsingLocalStorage": "Sačuvano korišćenjem lokalnog skladišta", + "errorWhileSavingImage": "Greška pri čuvanju slike", + "enterTextForYourLink": "Na primer, 'Saznajte više'", + "enterLinkURL": "Na primer, 'https://example.com'", + "enterValidImageURL": "Unesite validan URL slike", + "hex": "Hex", + "material": "Materijal", + "color": "Boja", + "savedUsingTheNetwork": "Sačuvano korišćenjem mreže", + "pleaseEnterTextForYourLink": "Unesite tekst za svoj link (na primer, 'Saznajte više')", + "pleaseEnterTheLinkURL": "Unesite URL linka (na primer, 'https://example.com')", + "pleaseEnterAValidImageURL": "Unesite važeći URL slike", + "pleaseEnterAValidVideoURL": "Unesite važeći URL videa", + "photo": "Foto", + "image": "Slika", + "caseSensitivityAndWholeWordSearch": "Osetljivost na velika i mala slova i potraga za celom rečju", + "insertImage": "Umetni sliku" +} + \ No newline at end of file diff --git a/lib/src/l10n/quill_sw.arb b/lib/src/l10n/quill_sw.arb new file mode 100644 index 00000000..e9149799 --- /dev/null +++ b/lib/src/l10n/quill_sw.arb @@ -0,0 +1,79 @@ +{ + "@@locale": "sw", + "pasteLink": "Bandika Kiungo", + "ok": "Sawa", + "selectColor": "Chagua Rangi", + "gallery": "Matunzio", + "link": "Kiungo", + "open": "Fungua", + "copy": "Nakili", + "remove": "Ondoa", + "save": "Hifadhi", + "zoom": "Kuza", + "saved": "Imehifadhiwa", + "text": "Maandishi", + "resize": "Badilisha Ukubwa", + "width": "Upana", + "height": "Urefu", + "size": "Ukubwa", + "small": "Ndogo", + "large": "Kubwa", + "huge": "Kubwa Sana", + "clear": "Wazi", + "font": "Fonti", + "search": "Tafuta", + "camera": "Kamera", + "video": "Video", + "undo": "Fanyua", + "redo": "Fanya Upya", + "fontFamily": "Familia ya Fonti", + "fontSize": "Ukubwa wa Fonti", + "bold": "Nono", + "subscript": "Maandishi ys Chini", + "superscript": "Maandishi ya Juu", + "italic": "Italiki", + "underline": "Pigia Mstari", + "strikeThrough": "Ghairi Maandishi", + "inlineCode": "Codi ya Laini Moja", + "fontColor": "Rangi ya Fonti", + "backgroundColor": "Rangi ya Nyuma", + "clearFormat": "Muundo Wazi", + "alignLeft": "Pangilia Kushoto", + "alignCenter": "Pangilia Kati", + "alignRight": "Pangilia Kulia", + "justifyWinWidth": "Kuhalalisha Upana wa Ushindi", + "textDirection": "Mwelekeo wa Maandishi", + "headerStyle": "Mtindo wa Mada", + "numberedList": "Orodha ya Nambari", + "bulletList": "Orodha ya Risasi", + "checkedList": "Orodha iliyoangaliwa", + "codeBlock": "aya ya codi", + "quote": "Nukuu", + "increaseIndent": "Ongeza Ujongezaji", + "decreaseIndent": "Punguza Ujongezaji", + "insertURL": "Ingiza Kiungo", + "visitLink": "Tembelea Kiungo", + "enterLink": "Ingiza Kiungo", + "enterMedia": "Ingiza Picha", + "edit": "Harir", + "apply": "Weka", + "hex": "Hexi", + "material": "Nyenzo", + "color": "Rangi", + "findText": "Pata Maandishi", + "moveToPreviousOccurrence": "Nenda Kwenye Tukio la Awali", + "moveToNextOccurrence": "Nenda kwa Tukio linalofuata", + "savedUsingNetwork": "Imehifadhiwa kwa Kutumia Mtandao", + "savedUsingLocalStorage": "Imehifadhiwa kwa Hifadhi ya Ndani", + "errorWhileSavingImage": "Hitilafu Wakati wa Kuhifadhi Picha", + "pleaseEnterTextForYourLink": "Kwa mfano, 'Jifunze zaidi'", + "pleaseEnterTheLinkURL": "Kwa mfano, 'https://example.com'", + "pleaseEnterAValidImageURL": "Tafadhali ingiza URL halali ya picha", + "savedUsingTheNetwork": "Imehifadhiwa kwa kutumia mtandao", + "pleaseEnterAValidVideoURL": "Tafadhali ingiza URL ya video ili", + "photo": "Picha", + "image": "Picha", + "caseSensitivityAndWholeWordSearch": "Uwiano wa herufi kubwa na ndogo na utafutaji wa neno zima", + "insertImage": "Weka Picha" + } + \ No newline at end of file diff --git a/lib/src/l10n/quill_tk.arb b/lib/src/l10n/quill_tk.arb new file mode 100644 index 00000000..e33ef49c --- /dev/null +++ b/lib/src/l10n/quill_tk.arb @@ -0,0 +1,79 @@ +{ + "@@locale": "tk", + "pasteLink": "Baglanyşygy goýuň", + "ok": "Bolýar", + "selectColor": "Reňk saýlaň", + "gallery": "Galereýa", + "link": "Baglanyşyk", + "open": "Aç", + "copy": "Kopýala", + "remove": "Poz", + "save": "Sakla", + "zoom": "Ulalt", + "saved": "Saklandy", + "text": "Tekst", + "resize": "Ölçegini üýtget", + "width": "In", + "height": "Boý", + "size": "Ölçegi", + "small": "Kiçi", + "large": "Uly", + "huge": "Has uly", + "clear": "Arassala", + "font": "Şrift", + "search": "Gözleg", + "camera": "Kamera", + "video": "Wideo", + "undo": "Yza al", + "redo": "Öňe al", + "fontFamily": "Şrift maşgalasy", + "fontSize": "Şrift ululygy", + "bold": "Galyň", + "subscript": "Aşaky ýazgy", + "superscript": "Ýokarky ýazgy", + "italic": "Italik", + "underline": "Aşagyny çyz", + "strikeThrough": "Üstüni çyz", + "inlineCode": "Bir setirde kod", + "fontColor": "Şrift reňki", + "backgroundColor": "Arka reňki", + "clearFormat": "Formaty arassala", + "alignLeft": "Çepe deňleşdir", + "alignCenter": "Orta deňleşdir", + "alignRight": "Saga deňleşdir", + "justifyWinWidth": "Justify win width", + "textDirection": "Tekst ugry", + "headerStyle": "Sözbaşy stili", + "numberedList": "Sanly sanaw", + "bulletList": "Okly sanawy", + "checkedList": "Tikli sanaw", + "codeBlock": "Kod blogy", + "quote": "Sitata", + "increaseIndent": "Indent köpelt", + "decreaseIndent": "Indent azalt", + "insertURL": "URL goý", + "visitLink": "Baglanyşyga giriň", + "enterLink": "Baglanyşyk giriň", + "enterMedia": "Mediýa giriziň", + "edit": "Üýtget", + "apply": "Ulan", + "hex": "Hex", + "material": "Material", + "color": "Reňk", + "findText": "Tekst tapyň", + "moveToPreviousOccurrence": "Öňki hadysa geçiň", + "moveToNextOccurrence": "Indiki hadysa geçiň", + "savedUsingNetwork": "Saved using the network", + "savedUsingLocalStorage": "Saved using the local storage", + "errorWhileSavingImage": "Error while saving image", + "pleaseEnterTextForYourLink": "Güýz öwrenmek)", + "pleaseEnterTheLinkURL": "https://example.com", + "pleaseEnterAValidImageURL": "Please enter a valid image URL", + "savedUsingTheNetwork": "Ulgama ulanyp saklanan", + "pleaseEnterAValidVideoURL": "Lütfen güýjük wideo URL giriziň", + "photo": "Surat", + "image": "Surat", + "caseSensitivityAndWholeWordSearch": "Iňkisar we iň oňg söz gözleýinç", + "insertImage": "Surat goş" + } + \ No newline at end of file diff --git a/lib/src/l10n/quill_tr.arb b/lib/src/l10n/quill_tr.arb new file mode 100644 index 00000000..b944ebda --- /dev/null +++ b/lib/src/l10n/quill_tr.arb @@ -0,0 +1,78 @@ +{ + "@@locale": "tr", + "pasteLink": "Bağlantıyı Yapıştır", + "ok": "Tamam", + "selectColor": "Renk Seçin", + "gallery": "Galeri", + "link": "Bağlantı", + "open": "Açık", + "copy": "Kopyala", + "remove": "Kaldır", + "save": "Kayıt Et", + "zoom": "Yakınlaştır", + "saved": "Kaydedildi", + "text": "Text", + "resize": "Yeniden Boyutlandır", + "width": "Genişlik", + "height": "Yükseklik", + "size": "Boyut", + "small": "Küçük", + "large": "Büyük", + "huge": "Daha Büyük", + "clear": "Temizle", + "font": "Yazı tipi", + "search": "Ara", + "camera": "Kamera", + "video": "Video", + "undo": "Geri", + "redo": "İleri", + "fontFamily": "Yazı Türü", + "fontSize": "Yazı Boyutu", + "bold": "Kalın", + "subscript": "Alt Simge", + "superscript": "Üst Simge", + "italic": "İtalik", + "underline": "Altı Çizili", + "strikeThrough": "Üsti Çizili", + "inlineCode": "Inline code", + "fontColor": "Yazı Rengi", + "backgroundColor": "Vurgu Rengi", + "clearFormat": "Formatı Temizle", + "alignLeft": "Sola Hizala", + "alignCenter": "Ortaya Hizala", + "alignRight": "Sağa Hizala", + "justifyWinWidth": "Kenarlara Hizala", + "textDirection": "Metin Yönü", + "headerStyle": "Başlık Stili", + "numberedList": "Numaralı Liste", + "bulletList": "Madde Listesi", + "checkedList": "Kontrol Listesi", + "codeBlock": "Kod Blogu", + "quote": "Alıntı", + "increaseIndent": "Girintiyi Artır", + "decreaseIndent": "Girintiyi Azalt", + "insertURL": "URL Giriniz", + "visitLink": "Bağlantıyı Ziyaret Et", + "enterLink": "Bağlantı Giriniz", + "enterMedia": "Medya Giriniz", + "edit": "Düzenle", + "apply": "Uygula", + "findText": "Find text", + "moveToPreviousOccurrence": "Move to previous occurrence", + "moveToNextOccurrence": "Move to next occurrence", + "savedUsingTheNetwork": "Saved using the network", + "savedUsingLocalStorage": "Saved using the local storage", + "errorWhileSavingImage": "Error while saving image", + "pleaseEnterTextForYourLink": "e.g., 'Learn more'", + "pleaseEnterTheLinkURL": "e.g., 'https://example.com'", + "pleaseEnterAValidImageURL": "Please enter a valid image URL", + "hex": "Hex", + "material": "Malzeme", + "color": "Renk", + "pleaseEnterAValidVideoURL": "Lütfen geçerli bir video URL'si girin", + "photo": "Fotoğraf", + "image": "Görüntü", + "caseSensitivityAndWholeWordSearch": "Büyük/küçük harf hassasiyeti ve tam kelime arama", + "insertImage": "Görüntü ekle" +} + \ No newline at end of file diff --git a/lib/src/l10n/quill_uk.arb b/lib/src/l10n/quill_uk.arb new file mode 100644 index 00000000..983a7ec0 --- /dev/null +++ b/lib/src/l10n/quill_uk.arb @@ -0,0 +1,78 @@ +{ + "@@locale": "uk", + "pasteLink": "Вставити посилання", + "ok": "ОК", + "selectColor": "Вибрати колір", + "gallery": "Галерея", + "link": "Посилання", + "open": "Відкрити", + "copy": "Копіювати", + "remove": "Видалити", + "save": "Зберегти", + "zoom": "Збільшити", + "saved": "Збережено", + "hex": "Hex", + "material": "Матеріал", + "color": "Колір", + "pleaseEnterAValidVideoURL": "Будь ласка, введіть дійсну URL-адресу відео", + "photo": "Фото", + "image": "Зображення", + "caseSensitivityAndWholeWordSearch": "Чутливість до регістру та пошук цілих слів", + "insertImage": "Вставити зображення", + "text": "Текст", + "resize": "Змінити розмір", + "width": "Ширина", + "height": "Висота", + "size": "Розмір", + "small": "Малий", + "large": "Великий", + "huge": "Величезний", + "clear": "Очистити", + "font": "Шрифт", + "search": "Пошук", + "camera": "Камера", + "video": "Відео", + "undo": "Скасувати", + "redo": "Повторити", + "fontFamily": "Сімейство шрифтів", + "fontSize": "Розмір шрифту", + "bold": "Жирний", + "subscript": "Нижній індекс", + "superscript": "Верхній індекс", + "italic": "Курсив", + "underline": "Підкреслити", + "strikeThrough": "Закреслений", + "inlineCode": "Вбудований код", + "fontColor": "Колір шрифту", + "backgroundColor": "Колір фону", + "clearFormat": "Очистити формат", + "alignLeft": "Вирівняти ліворуч", + "alignCenter": "Вирівняти по центру", + "alignRight": "Вирівняти праворуч", + "justifyWinWidth": "Вирівняти за шириною вікна", + "textDirection": "Напрямок тексту", + "headerStyle": "Стиль заголовка", + "numberedList": "Нумерований список", + "bulletList": "Маркований список", + "checkedList": "Список з позначками", + "codeBlock": "Блок коду", + "quote": "Цитата", + "increaseIndent": "Збільшити відступ", + "decreaseIndent": "Зменшити відступ", + "insertURL": "Вставити URL", + "visitLink": "Відвідати посилання", + "enterLink": "Ввести посилання", + "enterMedia": "Ввести медіа", + "edit": "Редагувати", + "apply": "Застосувати", + "findText": "Знайти текст", + "moveToPreviousOccurrence": "Перейти до попереднього випадку", + "moveToNextOccurrence": "Перейти до наступного випадку", + "savedUsingTheNetwork": "Збережено за допомогою мережі", + "savedUsingLocalStorage": "Збережено за допомогою локального сховища", + "errorWhileSavingImage": "Помилка при збереженні зображення", + "pleaseEnterTextForYourLink": "Наприклад, 'Дізнатися більше'", + "pleaseEnterTheLinkURL": "Наприклад, 'https://example.com'", + "pleaseEnterAValidImageURL": "Будь ласка, введіть правильний URL-адресу зображення" + } + \ No newline at end of file diff --git a/lib/src/l10n/quill_ur.arb b/lib/src/l10n/quill_ur.arb new file mode 100644 index 00000000..2acacf6f --- /dev/null +++ b/lib/src/l10n/quill_ur.arb @@ -0,0 +1,82 @@ +{ + "@@locale": "ur", + "pasteLink": "لنک پیسٹ کریں", + "ok": "ٹھیک ہے", + "selectColor": "رنگ منتخب کریں", + "gallery": "گیلری", + "link": "لنک", + "open": "کھولیں", + "copy": "نقل", + "remove": "ہٹا دیں", + "save": "محفوظ کریں", + "zoom": "زوم", + "saved": "محفوظ کر لیا", + "text": "متن", + "resize": "سائز تبدیل کریں۔", + "width": "چوڑائی", + "height": "اونچائی", + "size": "سائز", + "small": "چھوٹا", + "large": "بڑا", + "huge": "بہت بڑا", + "clear": "صاف", + "font": "فونٹ", + "search": "تلاش", + "camera": "کیمرا", + "video": "ویڈیو", + "undo": "واپس", + "redo": "دوبارہ", + "fontFamily": "فونٹ خاندان", + "fontSize": "فونٹ سائز", + "bold": "ڈہوکی", + "subscript": "نیچے لکھا", + "superscript": "اوپر لکھا", + "italic": "ٹیک کیا", + "underline": "نیچے خط", + "strikeThrough": "خط خوراک", + "inlineCode": "ان لائن کوڈ", + "fontColor": "فونٹ کا رنگ", + "backgroundColor": "پس منظر کا رنگ", + "clearFormat": "فارمیٹ صاف کریں", + "alignLeft": "بائیں ہم آہنگ ہوں", + "alignCenter": "مرکز میں ہم آہنگ ہوں", + "alignRight": "دائیں ہم آہنگ ہوں", + "justifyWinWidth": "جسٹیفائی ون چوڑائی", + "textDirection": "متن کی سمت", + "headerStyle": "ہیڈر کا انداز", + "numberedList": "مرقم فہرست", + "bulletList": "گولی فہرست", + "checkedList": "چیک کی گئی فہرست", + "codeBlock": "کوڈ بلاک", + "quote": "حوالہ", + "increaseIndent": "درجہ بڑھائیں", + "decreaseIndent": "درجہ گھٹائیں", + "insertURL": "یو آر ایل درج کریں", + "visitLink": "لنک دیکھیں", + "enterLink": "لنک درج کریں", + "enterMedia": "میڈیا درج کریں", + "edit": "ترتیب دیں", + "apply": "لگائیں", + "findText": "متن تلاش کریں", + "moveToPreviousOccurrence": "پچھلے واقعہ پر منتقل ہوں", + "moveToNextOccurrence": "اگلے واقعہ پر منتقل ہوں", + "savedUsingNetwork": "نیٹ ورک کا استعمال کر کے محفوظ ہوا", + "savedUsingLocalStorage": "مقامی ذخیرہ کار استعمال کر کے محفوظ ہوا", + "errorWhileSavingImage": "تصویر کو محفوظ کرتے وقت خطا", + "enterTextForYourLink": "مثال: 'مزید جانیں'", + "enterLinkURL": "مثال: 'https://example.com'", + "enterValidImageURL": "براہ کرم ایک درست تصویر URL درج کریں", + "hex": "ہیکس", + "material": "مواد", + "color": "رنگ", + "savedUsingTheNetwork": "نیٹ ورک کا استعمال کر کے محفوظ ہوا", + "pleaseEnterTextForYourLink": "براہ کرم اپنے لنک کے لیے متن درج کریں (مثال کے طور پر، 'مزید جانیں')", + "pleaseEnterTheLinkURL": "براہ کرم لنک کا URL درج کریں (مثال کے طور پر، 'https://example.com')", + "pleaseEnterAValidImageURL": "براہ کرم ایک درست تصویر URL درج کریں", + "pleaseEnterAValidVideoURL": "براہ کرم ایک درست ویڈیو URL درج کریں", + "photo": "تصویر", + "image": "تصویر", + "caseSensitivityAndWholeWordSearch": "معاملے کی حساسیت اور پورے الفاظ کی تلاش", + "insertImage": "تصویر داخل کریں" + } + \ No newline at end of file diff --git a/lib/src/l10n/quill_vi.arb b/lib/src/l10n/quill_vi.arb new file mode 100644 index 00000000..211b49a1 --- /dev/null +++ b/lib/src/l10n/quill_vi.arb @@ -0,0 +1,82 @@ +{ + "@@locale": "vi", + "pasteLink": "Chèn liên kết", + "ok": "Đồng ý", + "selectColor": "Chọn Màu", + "gallery": "Thư viện", + "link": "Liên kết", + "open": "Mở", + "copy": "Sao chép", + "remove": "Xoá", + "save": "Lưu", + "zoom": "Thu phóng", + "saved": "Đã lưu", + "text": "Chữ", + "resize": "Resize", + "width": "Rộng", + "height": "Cao", + "size": "Kích thước", + "small": "Nhỏ", + "large": "Lớn", + "huge": "Rất lớn", + "clear": "Xoá", + "font": "Phông chữ", + "search": "Tìm", + "camera": "Máy ảnh", + "video": "Video", + "undo": "Hoàn tác", + "redo": "Làm lại", + "fontFamily": "Phông chữ", + "fontSize": "Cỡ chữ", + "bold": "Đậm", + "subscript": "Chèn dưới", + "superscript": "Chèn trên", + "italic": "Nghiêng", + "underline": "Gạch chân", + "strikeThrough": "Gạch ngang", + "inlineCode": "Dòng mã", + "fontColor": "Màu chữ", + "backgroundColor": "Màu nền", + "clearFormat": "Xoá định dạng", + "alignLeft": "Căn trái", + "alignCenter": "Căn giữa", + "alignRight": "Căn phải", + "justifyWinWidth": "Căn đều chiều rộng", + "textDirection": "Hướng văn bản", + "headerStyle": "Kiểu tiêu đề", + "numberedList": "Danh sách có số", + "bulletList": "Danh sách định dạng", + "checkedList": "Danh sách kiểm tra", + "codeBlock": "Khối mã", + "quote": "Trích dẫn", + "increaseIndent": "Tăng độ lề", + "decreaseIndent": "Giảm độ lề", + "insertURL": "Chèn URL", + "visitLink": "Truy cập liên kết", + "enterLink": "Nhập liên kết", + "enterMedia": "Chèn phương tiện", + "edit": "Chỉnh sửa", + "apply": "Áp dụng", + "findText": "Tìm văn bản", + "moveToPreviousOccurrence": "Di chuyển đến lần xuất hiện trước", + "moveToNextOccurrence": "Di chuyển đến lần xuất hiện tiếp theo", + "savedUsingNetwork": "Đã lưu sử dụng mạng", + "savedUsingLocalStorage": "Đã lưu sử dụng lưu trữ địa phương", + "errorWhileSavingImage": "Lỗi khi lưu hình ảnh", + "enterTextForYourLink": "e.g., 'Tìm hiểu thêm'", + "enterLinkURL": "e.g., 'https://example.com'", + "enterValidImageURL": "Vui lòng nhập URL hình ảnh hợp lệ", + "hex": "Hex", + "material": "Chất liệu", + "color": "Màu", + "savedUsingTheNetwork": "Đã lưu bằng cách sử dụng mạng", + "pleaseEnterTextForYourLink": "Vui lòng nhập văn bản cho liên kết của bạn (ví dụ: 'Tìm hiểu thêm')", + "pleaseEnterTheLinkURL": "Vui lòng nhập URL của liên kết (ví dụ: 'https://example.com')", + "pleaseEnterAValidImageURL": "Vui lòng nhập URL hình ảnh hợp lệ", + "pleaseEnterAValidVideoURL": "Vui lòng nhập URL video hợp lệ", + "photo": "Ảnh", + "image": "Hình ảnh", + "caseSensitivityAndWholeWordSearch": "Độ nhạy cảm chữ hoa/chữ thường và tìm kiếm toàn bộ từ", + "insertImage": "Chèn hình ảnh" +} + \ No newline at end of file diff --git a/lib/src/l10n/quill_zh.arb b/lib/src/l10n/quill_zh.arb new file mode 100644 index 00000000..5f71a6a1 --- /dev/null +++ b/lib/src/l10n/quill_zh.arb @@ -0,0 +1,77 @@ +{ + "@@locale": "zh", + "pasteLink": "粘贴链接", + "ok": "确定", + "selectColor": "选择颜色", + "gallery": "相册", + "link": "链接", + "open": "打开", + "copy": "复制", + "remove": "移除", + "save": "保存", + "zoom": "缩放", + "saved": "已保存", + "text": "文本", + "resize": "调整大小", + "width": "宽度", + "height": "高度", + "size": "大小", + "small": "小", + "large": "大", + "huge": "巨大", + "clear": "清除", + "font": "字体", + "search": "搜索", + "camera": "相机", + "video": "视频", + "undo": "撤销", + "redo": "重做", + "fontFamily": "字体族", + "fontSize": "字号", + "bold": "加粗", + "subscript": "下标", + "superscript": "上标", + "italic": "斜体", + "underline": "下划线", + "strikeThrough": "删除线", + "inlineCode": "行内代码", + "fontColor": "字体颜色", + "backgroundColor": "背景颜色", + "clearFormat": "清除格式", + "alignLeft": "左对齐", + "alignCenter": "居中", + "alignRight": "右对齐", + "justifyWinWidth": "两端对齐", + "textDirection": "文本方向", + "headerStyle": "标题样式", + "numberedList": "编号列表", + "bulletList": "项目符号列表", + "checkedList": "选中列表", + "codeBlock": "代码块", + "quote": "引用", + "increaseIndent": "增加缩进", + "decreaseIndent": "减少缩进", + "insertURL": "插入网址", + "visitLink": "访问链接", + "enterLink": "输入链接", + "enterMedia": "输入媒体", + "edit": "编辑", + "apply": "应用", + "hex": "十六进制", + "material": "素材", + "color": "颜色", + "findText": "查找文本", + "moveToPreviousOccurrence": "移到前一个匹配项", + "moveToNextOccurrence": "移到下一个匹配项", + "savedUsingTheNetwork": "使用网络保存", + "savedUsingLocalStorage": "使用本地存储保存", + "errorWhileSavingImage": "保存图像时出错", + "pleaseEnterTextForYourLink": "请输入链接文本(例如,'了解更多')", + "pleaseEnterTheLinkURL": "请输入链接网址(例如,'https://example.com')", + "pleaseEnterAValidImageURL": "请输入有效的图像网址", + "photo": "照片", + "image": "图像", + "pleaseEnterAValidVideoURL": "请输入有效的视频URL", + "caseSensitivityAndWholeWordSearch": "区分大小写和整词搜索", + "insertImage": "插入图像" +} diff --git a/lib/src/l10n/quill_zh_CN.arb b/lib/src/l10n/quill_zh_CN.arb new file mode 100644 index 00000000..51e67c40 --- /dev/null +++ b/lib/src/l10n/quill_zh_CN.arb @@ -0,0 +1,78 @@ +{ + "@@locale": "zh_CN", + "pasteLink": "粘贴链接", + "ok": "好", + "selectColor": "选择颜色", + "gallery": "相簿", + "link": "链接", + "open": "打开", + "copy": "复制", + "remove": "移除", + "save": "保存", + "zoom": "放大", + "saved": "已保存", + "text": "文字", + "resize": "调整大小", + "width": "宽度", + "height": "高度", + "size": "文字大小", + "small": "小字号", + "large": "大字号", + "huge": "超大字号", + "clear": "清除", + "font": "字体", + "search": "搜索", + "camera": "拍照", + "video": "录像", + "undo": "撤销", + "redo": "重做", + "fontFamily": "字体", + "fontSize": "字号", + "bold": "粗体", + "subscript": "下标", + "superscript": "上标", + "italic": "斜体", + "underline": "下划线", + "strikeThrough": "删除线", + "inlineCode": "内联代码", + "fontColor": "字体颜色", + "backgroundColor": "背景颜色", + "clearFormat": "清除格式", + "alignLeft": "左对齐", + "alignCenter": "居中对齐", + "alignRight": "右对齐", + "justifyWinWidth": "两端对齐", + "textDirection": "文本方向", + "headerStyle": "标题样式", + "numberedList": "有序列表", + "bulletList": "无序列表", + "checkedList": "任务列表", + "codeBlock": "代码块", + "quote": "引言", + "increaseIndent": "增加缩进", + "decreaseIndent": "减少缩进", + "insertURL": "插入链接", + "visitLink": "访问链接", + "enterLink": "输入链接", + "enterMedia": "输入媒体", + "edit": "编辑", + "apply": "应用", + "findText": "搜索文本", + "moveToPreviousOccurrence": "上一个匹配项", + "moveToNextOccurrence": "下一个匹配项", + "savedUsingTheNetwork": "通过网络保存", + "savedUsingLocalStorage": "使用本地存储保存", + "errorWhileSavingImage": "保存图像时发生错误", + "pleaseEnterTextForYourLink": "例如,'了解更多'", + "pleaseEnterTheLinkURL": "例如,'https://example.com'", + "pleaseEnterAValidImageURL": "请输入有效的图像URL", + "hex": "十六进制", + "material": "材料", + "color": "颜色", + "pleaseEnterAValidVideoURL": "请输入有效的视频URL", + "photo": "照片", + "image": "图像", + "caseSensitivityAndWholeWordSearch": "区分大小写和整词搜索", + "insertImage": "插入图像" +} + \ No newline at end of file diff --git a/lib/src/l10n/quill_zh_HK.arb b/lib/src/l10n/quill_zh_HK.arb new file mode 100644 index 00000000..7c2ae470 --- /dev/null +++ b/lib/src/l10n/quill_zh_HK.arb @@ -0,0 +1,78 @@ +{ + "@@locale": "zh_HK", + "pasteLink": "貼上連結", + "ok": "確定", + "selectColor": "選擇顏色", + "gallery": "圖片庫", + "link": "連結", + "open": "開啓", + "copy": "複製", + "remove": "移除", + "save": "儲存", + "zoom": "放大", + "saved": "已儲存", + "text": "文字", + "resize": "變更大小", + "width": "寛", + "height": "高", + "size": "大小", + "small": "小", + "large": "大", + "huge": "超大", + "clear": "清除", + "font": "字型", + "search": "搜尋", + "camera": "相機", + "video": "錄影", + "undo": "撤銷", + "redo": "重做", + "fontFamily": "字體", + "fontSize": "字號", + "bold": "粗體", + "subscript": "下標", + "superscript": "上標", + "italic": "斜體", + "underline": "下劃線", + "strikeThrough": "刪除線", + "inlineCode": "內聯代碼", + "fontColor": "字體顏色", + "backgroundColor": "背景顏色", + "clearFormat": "清除格式", + "alignLeft": "左對齊", + "alignCenter": "居中對齊", + "alignRight": "右對齊", + "justifyWinWidth": "兩端對齊", + "textDirection": "文本方向", + "headerStyle": "標題樣式", + "numberedList": "有序列表", + "bulletList": "無序列表", + "checkedList": "任務列表", + "codeBlock": "代碼塊", + "quote": "引言", + "increaseIndent": "增加縮進", + "decreaseIndent": "減少縮進", + "insertURL": "插入鏈接", + "visitLink": "訪問鏈接", + "enterLink": "輸入鏈接", + "enterMedia": "輸入媒體", + "edit": "編輯", + "apply": "應用", + "findText": "搜尋文本", + "moveToPreviousOccurrence": "上一個匹配項", + "moveToNextOccurrence": "下一個匹配項", + "savedUsingTheNetwork": "通過網絡保存", + "savedUsingLocalStorage": "使用本地存儲保存", + "errorWhileSavingImage": "保存圖像時發生錯誤", + "pleaseEnterTextForYourLink": "例如,'了解更多'", + "pleaseEnterTheLinkURL": "例如,'https://example.com'", + "pleaseEnterAValidImageURL": "請輸入有效的圖像URL", + "hex": "十六進制", + "material": "物料", + "color": "顏色", + "pleaseEnterAValidVideoURL": "請輸入有效的視頻URL", + "photo": "照片", + "image": "圖像", + "caseSensitivityAndWholeWordSearch": "區分大小寫和整詞搜索", + "insertImage": "插入圖像" +} + \ No newline at end of file diff --git a/lib/src/l10n/untranslated.json b/lib/src/l10n/untranslated.json new file mode 100644 index 00000000..9e26dfee --- /dev/null +++ b/lib/src/l10n/untranslated.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/lib/src/l10n/widgets/localizations.dart b/lib/src/l10n/widgets/localizations.dart new file mode 100644 index 00000000..7e40894d --- /dev/null +++ b/lib/src/l10n/widgets/localizations.dart @@ -0,0 +1,28 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; + +import '../../extensions/quill_provider.dart'; +import '../extensions/localizations.dart'; + +class FlutterQuillLocalizationsWidget extends StatelessWidget { + const FlutterQuillLocalizationsWidget({ + required this.child, + super.key, + }); + + final Widget child; + + @override + Widget build(BuildContext context) { + final loc = FlutterQuillLocalizations.of(context); + if (loc != null) { + return child; + } + return Localizations( + locale: context.requireQuillSharedConfigurations.locale ?? + Localizations.localeOf(context), + delegates: FlutterQuillLocalizations.localizationsDelegates, + child: child, + ); + } +} diff --git a/lib/src/models/config/editor/configurations.dart b/lib/src/models/config/editor/configurations.dart index 464689de..c6e0f5de 100644 --- a/lib/src/models/config/editor/configurations.dart +++ b/lib/src/models/config/editor/configurations.dart @@ -2,12 +2,14 @@ import 'package:equatable/equatable.dart'; import 'package:flutter/foundation.dart' show Brightness, Uint8List, immutable; import 'package:flutter/material.dart' - show TextCapitalization, TextSelectionThemeData; + show TextCapitalization, TextInputAction, TextSelectionThemeData; import 'package:flutter/widgets.dart'; +import 'package:meta/meta.dart' show experimental; import '../../../widgets/default_styles.dart'; import '../../../widgets/delegate.dart'; import '../../../widgets/editor/editor.dart'; +import '../../../widgets/editor/editor_builder.dart'; import '../../../widgets/embeds.dart'; import '../../../widgets/link.dart'; import '../../../widgets/raw_editor/raw_editor.dart'; @@ -67,6 +69,9 @@ class QuillEditorConfigurations extends Equatable { this.editorKey, this.requestKeyboardFocusOnCheckListChanged = false, this.elementOptions = const QuillEditorElementOptions(), + this.builder, + this.magnifierConfiguration, + this.textInputAction = TextInputAction.newline, }); /// The text placeholder in the quill editor @@ -306,6 +311,15 @@ class QuillEditorConfigurations extends Equatable { /// This is not complete yet and might changed final QuillEditorElementOptions elementOptions; + final QuillEditorBuilder? builder; + + /// Currently this feature is experimental + @experimental + final TextMagnifierConfiguration? magnifierConfiguration; + + /// Default to [TextInputAction.newline] + final TextInputAction textInputAction; + @override List get props => [ placeholder, @@ -323,7 +337,7 @@ class QuillEditorConfigurations extends Equatable { double? scrollBottomInset, EdgeInsetsGeometry? padding, bool? autoFocus, - bool? enableUnfocusOnTapOutside, + bool? isOnTapOutsideEnabled, Function(PointerDownEvent event, FocusNode focusNode)? onTapOutside, bool? showCursor, bool? paintCursorAboveText, @@ -357,6 +371,9 @@ class QuillEditorConfigurations extends Equatable { TextSelectionThemeData? textSelectionThemeData, bool? requestKeyboardFocusOnCheckListChanged, QuillEditorElementOptions? elementOptions, + QuillEditorBuilder? builder, + TextMagnifierConfiguration? magnifierConfiguration, + TextInputAction? textInputAction, }) { return QuillEditorConfigurations( placeholder: placeholder ?? this.placeholder, @@ -365,7 +382,8 @@ class QuillEditorConfigurations extends Equatable { scrollBottomInset: scrollBottomInset ?? this.scrollBottomInset, padding: padding ?? this.padding, autoFocus: autoFocus ?? this.autoFocus, - isOnTapOutsideEnabled: enableUnfocusOnTapOutside ?? isOnTapOutsideEnabled, + isOnTapOutsideEnabled: + isOnTapOutsideEnabled ?? this.isOnTapOutsideEnabled, onTapOutside: onTapOutside ?? this.onTapOutside, showCursor: showCursor ?? this.showCursor, paintCursorAboveText: paintCursorAboveText ?? this.paintCursorAboveText, @@ -409,6 +427,10 @@ class QuillEditorConfigurations extends Equatable { requestKeyboardFocusOnCheckListChanged ?? this.requestKeyboardFocusOnCheckListChanged, elementOptions: elementOptions ?? this.elementOptions, + builder: builder ?? this.builder, + magnifierConfiguration: + magnifierConfiguration ?? this.magnifierConfiguration, + textInputAction: textInputAction ?? this.textInputAction, ); } } diff --git a/lib/src/models/config/others/animations.dart b/lib/src/models/config/others/animations.dart index 829442c5..ce6bcb0c 100644 --- a/lib/src/models/config/others/animations.dart +++ b/lib/src/models/config/others/animations.dart @@ -1,10 +1,8 @@ import 'package:equatable/equatable.dart'; -import 'package:flutter/foundation.dart' show immutable; - -import '../../../utils/experimental.dart'; +import 'package:meta/meta.dart' show experimental, immutable; @immutable -@Experimental('This class might removed') +@experimental class QuillAnimationConfigurations extends Equatable { const QuillAnimationConfigurations({ required this.checkBoxPointItem, diff --git a/lib/src/models/config/raw_editor/configurations.dart b/lib/src/models/config/raw_editor/configurations.dart new file mode 100644 index 00000000..b7bdebe3 --- /dev/null +++ b/lib/src/models/config/raw_editor/configurations.dart @@ -0,0 +1,290 @@ +import 'package:equatable/equatable.dart'; +import 'package:flutter/foundation.dart' show Brightness, Uint8List; +import 'package:flutter/material.dart' + show + AdaptiveTextSelectionToolbar, + PointerDownEvent, + TextCapitalization, + TextInputAction; +import 'package:flutter/widgets.dart' + show + Action, + BuildContext, + Color, + ContentInsertionConfiguration, + EdgeInsets, + EdgeInsetsGeometry, + FocusNode, + Intent, + ScrollController, + ScrollPhysics, + ShortcutActivator, + TextFieldTapRegion, + TextSelectionControls, + ValueChanged, + Widget; +import 'package:meta/meta.dart' show immutable; + +import '../../../widgets/controller.dart'; +import '../../../widgets/cursor.dart'; +import '../../../widgets/default_styles.dart'; +import '../../../widgets/delegate.dart'; +import '../../../widgets/link.dart'; +import '../../../widgets/raw_editor/raw_editor.dart'; +import '../../../widgets/raw_editor/raw_editor_state.dart'; +import '../../themes/quill_dialog_theme.dart'; + +@immutable +class QuillRawEditorConfigurations extends Equatable { + const QuillRawEditorConfigurations({ + required this.controller, + required this.focusNode, + required this.scrollController, + required this.scrollBottomInset, + required this.cursorStyle, + required this.selectionColor, + required this.selectionCtrls, + required this.embedBuilder, + required this.autoFocus, + this.showCursor = true, + this.scrollable = true, + this.padding = EdgeInsets.zero, + this.isReadOnly = false, + this.placeholder, + this.onLaunchUrl, + this.contextMenuBuilder = defaultContextMenuBuilder, + this.showSelectionHandles = false, + this.textCapitalization = TextCapitalization.none, + this.maxHeight, + this.minHeight, + this.maxContentWidth, + this.customStyles, + this.customShortcuts, + this.customActions, + this.expands = false, + this.isOnTapOutsideEnabled = true, + this.onTapOutside, + this.keyboardAppearance = Brightness.light, + this.enableInteractiveSelection = true, + this.scrollPhysics, + this.linkActionPickerDelegate = defaultLinkActionPickerDelegate, + this.customStyleBuilder, + this.customRecognizerBuilder, + this.floatingCursorDisabled = false, + this.onImagePaste, + this.customLinkPrefixes = const [], + this.dialogTheme, + this.contentInsertionConfiguration, + this.textInputAction = TextInputAction.newline, + this.requestKeyboardFocusOnCheckListChanged = false, + }); + + /// Controls the document being edited. + final QuillController controller; + + /// Controls whether this editor has keyboard focus. + final FocusNode focusNode; + final ScrollController scrollController; + final bool scrollable; + final double scrollBottomInset; + + /// Additional space around the editor contents. + final EdgeInsetsGeometry padding; + + /// Whether the text can be changed. + /// + /// When this is set to true, the text cannot be modified + /// by any shortcut or keyboard operation. The text is still selectable. + /// + /// Defaults to false. Must not be null. + final bool isReadOnly; + + final String? placeholder; + + /// Callback which is triggered when the user wants to open a URL from + /// a link in the document. + final ValueChanged? onLaunchUrl; + + /// Builds the text selection toolbar when requested by the user. + /// + /// See also: + /// * [EditableText.contextMenuBuilder], which builds the default + /// text selection toolbar for [EditableText]. + /// + /// If not provided, no context menu will be shown. + final QuillEditorContextMenuBuilder? contextMenuBuilder; + + static Widget defaultContextMenuBuilder( + BuildContext context, + QuillRawEditorState state, + ) { + return TextFieldTapRegion( + child: AdaptiveTextSelectionToolbar.buttonItems( + buttonItems: state.contextMenuButtonItems, + anchors: state.contextMenuAnchors, + ), + ); + } + + /// Whether to show selection handles. + /// + /// When a selection is active, there will be two handles at each side of + /// boundary, or one handle if the selection is collapsed. The handles can be + /// dragged to adjust the selection. + /// + /// See also: + /// + /// * [showCursor], which controls the visibility of the cursor. + final bool showSelectionHandles; + + /// Whether to show cursor. + /// + /// The cursor refers to the blinking caret when the editor is focused. + /// + /// See also: + /// + /// * [cursorStyle], which controls the cursor visual representation. + /// * [showSelectionHandles], which controls the visibility of the selection + /// handles. + final bool showCursor; + + /// The style to be used for the editing cursor. + final CursorStyle cursorStyle; + + /// Configures how the platform keyboard will select an uppercase or + /// lowercase keyboard. + /// + /// Only supports text keyboards, other keyboard types will ignore this + /// configuration. Capitalization is locale-aware. + /// + /// Defaults to [TextCapitalization.none]. Must not be null. + /// + /// See also: + /// + /// * [TextCapitalization], for a description of each capitalization behavior + final TextCapitalization textCapitalization; + + /// The maximum height this editor can have. + /// + /// If this is null then there is no limit to the editor's height and it will + /// expand to fill its parent. + final double? maxHeight; + + /// The minimum height this editor can have. + final double? minHeight; + + /// The maximum width to be occupied by the content of this editor. + /// + /// If this is not null and and this editor's width is larger than this value + /// then the contents will be constrained to the provided maximum width and + /// horizontally centered. This is mostly useful on devices with wide screens. + final double? maxContentWidth; + + /// Allows to override [DefaultStyles]. + final DefaultStyles? customStyles; + + /// Whether this widget's height will be sized to fill its parent. + /// + /// If set to true and wrapped in a parent widget like [Expanded] or + /// + /// Defaults to false. + final bool expands; + + /// Whether this editor should focus itself if nothing else is already + /// focused. + /// + /// If true, the keyboard will open as soon as this text field obtains focus. + /// Otherwise, the keyboard is only shown after the user taps the text field. + /// + /// Defaults to false. Cannot be null. + final bool autoFocus; + + /// The color to use when painting the selection. + final Color selectionColor; + + /// Delegate for building the text selection handles and toolbar. + /// + /// The [QuillRawEditor] widget used on its own will not trigger the display + /// of the selection toolbar by itself. The toolbar is shown by calling + /// [QuillRawEditorState.showToolbar] in response to + /// an appropriate user event. + final TextSelectionControls selectionCtrls; + + /// The appearance of the keyboard. + /// + /// This setting is only honored on iOS devices. + /// + /// Defaults to [Brightness.light]. + final Brightness keyboardAppearance; + + /// If true, then long-pressing this TextField will select text and show the + /// cut/copy/paste menu, and tapping will move the text caret. + /// + /// True by default. + /// + /// If false, most of the accessibility support for selecting text, copy + /// and paste, and moving the caret will be disabled. + final bool enableInteractiveSelection; + + bool get selectionEnabled => enableInteractiveSelection; + + /// The [ScrollPhysics] to use when vertically scrolling the input. + /// + /// If not specified, it will behave according to the current platform. + /// + /// See [Scrollable.physics]. + final ScrollPhysics? scrollPhysics; + + final Future Function(Uint8List imageBytes)? onImagePaste; + + /// Contains user-defined shortcuts map. + /// + /// [https://docs.flutter.dev/development/ui/advanced/actions-and-shortcuts#shortcuts] + final Map? customShortcuts; + + /// Contains user-defined actions. + /// + /// [https://docs.flutter.dev/development/ui/advanced/actions-and-shortcuts#actions] + final Map>? customActions; + + /// Builder function for embeddable objects. + final EmbedsBuilder embedBuilder; + final LinkActionPickerDelegate linkActionPickerDelegate; + final CustomStyleBuilder? customStyleBuilder; + final CustomRecognizerBuilder? customRecognizerBuilder; + final bool floatingCursorDisabled; + final List customLinkPrefixes; + + /// Configures the dialog theme. + final QuillDialogTheme? dialogTheme; + + /// Configuration of handler for media content inserted via the system input + /// method. + /// + /// See [https://api.flutter.dev/flutter/widgets/EditableText/contentInsertionConfiguration.html] + final ContentInsertionConfiguration? contentInsertionConfiguration; + + /// Whether the [onTapOutside] should be triggered or not + /// Defaults to `true` + /// it have default implementation, check [onTapOuside] for more + final bool isOnTapOutsideEnabled; + + /// This will run only when [isOnTapOutsideEnabled] is true + /// by default on desktop and web it will unfocus + /// on mobile it will only unFocus if the kind property of + /// event [PointerDownEvent] is [PointerDeviceKind.unknown] + /// you can override this to fit your needs + final Function(PointerDownEvent event, FocusNode focusNode)? onTapOutside; + + /// When there is a change the check list values + /// should we request keyboard focus?? + final bool requestKeyboardFocusOnCheckListChanged; + + final TextInputAction textInputAction; + + @override + List get props => [ + isReadOnly, + placeholder, + ]; +} diff --git a/lib/src/models/config/shared_configurations.dart b/lib/src/models/config/shared_configurations.dart index 78dd6b06..edad5404 100644 --- a/lib/src/models/config/shared_configurations.dart +++ b/lib/src/models/config/shared_configurations.dart @@ -18,6 +18,7 @@ class QuillSharedConfigurations extends Equatable { this.animationConfigurations = const QuillAnimationConfigurations( checkBoxPointItem: false, ), + this.extraConfigurations = const {}, }); // This is just example or showcase of this major update to make the library @@ -30,12 +31,18 @@ class QuillSharedConfigurations extends Equatable { final QuillDialogTheme? dialogTheme; /// The locale to use for the editor and toolbar, defaults to system locale - /// More https://github.com/singerdmx/flutter-quill#translation + /// More https://github.com/singerdmx/flutter-quill/blob/master/doc/translation.md + /// this won't used if you defined the [FlutterQuillLocalizations.delegate] + /// in the `localizationsDelegates` which exists in + /// `MaterialApp` or `WidgetsApp` final Locale? locale; /// To configure which animations you want to be enabled final QuillAnimationConfigurations animationConfigurations; + /// Store custom configurations in here and use it in the widget tree + final Map extraConfigurations; + @override List get props => [ dialogBarrierColor, diff --git a/lib/src/models/config/toolbar/base_configurations.dart b/lib/src/models/config/toolbar/base_configurations.dart index 132fdc8d..1c4d1ff3 100644 --- a/lib/src/models/config/toolbar/base_configurations.dart +++ b/lib/src/models/config/toolbar/base_configurations.dart @@ -4,7 +4,6 @@ import 'package:flutter/widgets.dart' import '../../../widgets/toolbar/base_toolbar.dart'; import '../../structs/link_dialog_action.dart'; -import '../../themes/quill_custom_button.dart'; @immutable class QuillBaseToolbarConfigurations extends Equatable { @@ -41,7 +40,7 @@ class QuillBaseToolbarConfigurations extends Equatable { final Color? color; /// List of custom buttons - final List customButtons; + final List customButtons; /// The color to use when painting the toolbar section divider. /// diff --git a/lib/src/models/config/toolbar/buttons/base.dart b/lib/src/models/config/toolbar/buttons/base.dart index 931b6fa3..2eadbff8 100644 --- a/lib/src/models/config/toolbar/buttons/base.dart +++ b/lib/src/models/config/toolbar/buttons/base.dart @@ -5,7 +5,8 @@ import 'package:flutter/widgets.dart' show BuildContext, IconData, Widget; import '../../../../../flutter_quill.dart' show QuillController, QuillProvider; import '../../../themes/quill_icon_theme.dart' show QuillIconTheme; -import '../../quill_configurations.dart' show kDefaultIconSize; +import '../../quill_configurations.dart' + show kDefaultIconSize, kIconButtonFactor; @immutable class QuillToolbarBaseButtonExtraOptions extends Equatable { @@ -38,6 +39,7 @@ class QuillToolbarBaseButtonOptions extends Equatable { const QuillToolbarBaseButtonOptions({ this.iconData, this.globalIconSize = kDefaultIconSize, + this.globalIconButtonFactor = kIconButtonFactor, this.afterButtonPressed, this.tooltip, this.iconTheme, @@ -51,10 +53,14 @@ class QuillToolbarBaseButtonOptions extends Equatable { final IconData? iconData; /// To change the the icon size pass a different value, by default will be - /// [kDefaultIconSize] + /// [kDefaultIconSize]. /// this will be used for all the buttons but you can override this final double globalIconSize; + /// The factor of how much larger the button is in relation to the icon, + /// by default it will be [kIconButtonFactor]. + final double globalIconButtonFactor; + /// To do extra logic after pressing the button final VoidCallback? afterButtonPressed; diff --git a/lib/src/models/config/toolbar/buttons/clear_format.dart b/lib/src/models/config/toolbar/buttons/clear_format.dart index f38dd52f..d34eef84 100644 --- a/lib/src/models/config/toolbar/buttons/clear_format.dart +++ b/lib/src/models/config/toolbar/buttons/clear_format.dart @@ -20,7 +20,9 @@ class QuillToolbarClearFormatButtonOptions super.iconTheme, super.tooltip, this.iconSize, + this.iconButtonFactor, }); final double? iconSize; + final double? iconButtonFactor; } diff --git a/lib/src/models/config/toolbar/buttons/color.dart b/lib/src/models/config/toolbar/buttons/color.dart index 19bac781..e6c2f9c5 100644 --- a/lib/src/models/config/toolbar/buttons/color.dart +++ b/lib/src/models/config/toolbar/buttons/color.dart @@ -1,6 +1,7 @@ import 'package:flutter/widgets.dart' show Color; -import './../../shared_configurations.dart' show QuillSharedConfigurations; +import './../../shared_configurations.dart' show QuillSharedConfigurations; +import '../../../../widgets/controller.dart'; import 'base.dart'; class QuillToolbarColorButtonExtraOptions @@ -26,18 +27,27 @@ class QuillToolbarColorButtonOptions extends QuillToolbarBaseButtonOptions< const QuillToolbarColorButtonOptions({ this.dialogBarrierColor, this.iconSize, + this.iconButtonFactor, super.iconData, super.afterButtonPressed, super.childBuilder, super.controller, - super.globalIconSize, super.iconTheme, super.tooltip, + this.customOnPressedCallback, }); final double? iconSize; + final double? iconButtonFactor; /// By default will use the default `dialogBarrierColor` from /// [QuillSharedConfigurations] final Color? dialogBarrierColor; + + final QuillToolbarColorPickerOnPressedCallback? customOnPressedCallback; } + +typedef QuillToolbarColorPickerOnPressedCallback = Future Function( + QuillController controller, + bool isBackground, +); diff --git a/lib/src/models/config/toolbar/buttons/custom_button.dart b/lib/src/models/config/toolbar/buttons/custom_button.dart new file mode 100644 index 00000000..9a862e83 --- /dev/null +++ b/lib/src/models/config/toolbar/buttons/custom_button.dart @@ -0,0 +1,32 @@ +import 'package:flutter/widgets.dart' show VoidCallback, Widget; + +import 'base.dart'; + +class QuillToolbarCustomButtonExtraOptions + extends QuillToolbarBaseButtonExtraOptions { + const QuillToolbarCustomButtonExtraOptions({ + required super.controller, + required super.context, + required super.onPressed, + }); +} + +class QuillToolbarCustomButtonOptions extends QuillToolbarBaseButtonOptions< + QuillToolbarBaseButtonOptions, QuillToolbarCustomButtonExtraOptions> { + const QuillToolbarCustomButtonOptions({ + this.icon, + this.iconButtonFactor, + this.iconSize, + super.afterButtonPressed, + super.tooltip, + super.iconTheme, + super.childBuilder, + super.controller, + this.onPressed, + }); + + final double? iconButtonFactor; + final double? iconSize; + final VoidCallback? onPressed; + final Widget? icon; +} diff --git a/lib/src/models/config/toolbar/buttons/font_family.dart b/lib/src/models/config/toolbar/buttons/font_family.dart index 6b79685f..41319bbb 100644 --- a/lib/src/models/config/toolbar/buttons/font_family.dart +++ b/lib/src/models/config/toolbar/buttons/font_family.dart @@ -50,18 +50,16 @@ class QuillToolbarFontFamilyButtonOptions extends QuillToolbarBaseButtonOptions< this.itemPadding, this.defaultItemColor = Colors.red, this.renderFontFamilies = true, - @Deprecated('It is not required because of `rawItemsMap`') this.items, this.highlightElevation = 1, this.hoverElevation = 1, this.fillColor, this.iconSize, + this.iconButtonFactor, }); final Color? fillColor; final double hoverElevation; final double highlightElevation; - @Deprecated('It is not required because of `rawItemsMap`') - final List>? items; /// By default it will be [fontFamilyValues] from [QuillToolbarConfigurations] /// You can override this if you want @@ -82,6 +80,7 @@ class QuillToolbarFontFamilyButtonOptions extends QuillToolbarBaseButtonOptions< /// By default will use [globalIconSize] final double? iconSize; + final double? iconButtonFactor; QuillToolbarFontFamilyButtonOptions copyWith({ Color? fillColor, @@ -102,6 +101,7 @@ class QuillToolbarFontFamilyButtonOptions extends QuillToolbarBaseButtonOptions< EdgeInsets? itemPadding, Color? defaultItemColor, double? iconSize, + double? iconButtonFactor, // Add properties to override inherited properties QuillController? controller, IconData? iconData, @@ -130,11 +130,10 @@ class QuillToolbarFontFamilyButtonOptions extends QuillToolbarBaseButtonOptions< itemPadding: itemPadding ?? this.itemPadding, defaultItemColor: defaultItemColor ?? this.defaultItemColor, iconSize: iconSize ?? this.iconSize, + iconButtonFactor: iconButtonFactor ?? this.iconButtonFactor, fillColor: fillColor ?? this.fillColor, hoverElevation: hoverElevation ?? this.hoverElevation, highlightElevation: highlightElevation ?? this.highlightElevation, - // ignore: deprecated_member_use_from_same_package - items: items ?? this.items, ); } } diff --git a/lib/src/models/config/toolbar/buttons/font_size.dart b/lib/src/models/config/toolbar/buttons/font_size.dart index e9d4d5e7..45464ecf 100644 --- a/lib/src/models/config/toolbar/buttons/font_size.dart +++ b/lib/src/models/config/toolbar/buttons/font_size.dart @@ -30,10 +30,10 @@ class QuillToolbarFontSizeButtonOptions extends QuillToolbarBaseButtonOptions< QuillToolbarFontSizeButtonOptions, QuillToolbarFontSizeButtonExtraOptions> { const QuillToolbarFontSizeButtonOptions({ this.iconSize, + this.iconButtonFactor, this.fillColor, this.hoverElevation = 1, this.highlightElevation = 1, - this.items, this.rawItemsMap, this.onSelected, super.iconTheme, @@ -53,11 +53,10 @@ class QuillToolbarFontSizeButtonOptions extends QuillToolbarBaseButtonOptions< }); final double? iconSize; + final double? iconButtonFactor; final Color? fillColor; final double hoverElevation; final double highlightElevation; - @Deprecated('It is not required because of `rawItemsMap`') - final List>? items; /// By default it will be [fontSizesValues] from [QuillToolbarConfigurations] /// You can override this if you want @@ -75,6 +74,7 @@ class QuillToolbarFontSizeButtonOptions extends QuillToolbarBaseButtonOptions< QuillToolbarFontSizeButtonOptions copyWith({ double? iconSize, + double? iconButtonFactor, Color? fillColor, double? hoverElevation, double? highlightElevation, @@ -97,11 +97,10 @@ class QuillToolbarFontSizeButtonOptions extends QuillToolbarBaseButtonOptions< }) { return QuillToolbarFontSizeButtonOptions( iconSize: iconSize ?? this.iconSize, + iconButtonFactor: iconButtonFactor ?? this.iconButtonFactor, fillColor: fillColor ?? this.fillColor, hoverElevation: hoverElevation ?? this.hoverElevation, highlightElevation: highlightElevation ?? this.highlightElevation, - // ignore: deprecated_member_use_from_same_package - items: items ?? this.items, rawItemsMap: rawItemsMap ?? this.rawItemsMap, onSelected: onSelected ?? this.onSelected, attribute: attribute ?? this.attribute, diff --git a/lib/src/models/config/toolbar/buttons/history.dart b/lib/src/models/config/toolbar/buttons/history.dart index eeceeda5..1f1bfaab 100644 --- a/lib/src/models/config/toolbar/buttons/history.dart +++ b/lib/src/models/config/toolbar/buttons/history.dart @@ -28,6 +28,7 @@ class QuillToolbarHistoryButtonOptions extends QuillToolbarBaseButtonOptions< super.tooltip, super.childBuilder, this.iconSize, + this.iconButtonFactor, }); /// If this true then it will be the undo button @@ -36,4 +37,5 @@ class QuillToolbarHistoryButtonOptions extends QuillToolbarBaseButtonOptions< /// By default will use [globalIconSize] final double? iconSize; + final double? iconButtonFactor; } diff --git a/lib/src/models/config/toolbar/buttons/indent.dart b/lib/src/models/config/toolbar/buttons/indent.dart index 30252088..66700c71 100644 --- a/lib/src/models/config/toolbar/buttons/indent.dart +++ b/lib/src/models/config/toolbar/buttons/indent.dart @@ -21,7 +21,9 @@ class QuillToolbarIndentButtonOptions extends QuillToolbarBaseButtonOptions { super.iconTheme, super.tooltip, this.iconSize, + this.iconButtonFactor, }); final double? iconSize; + final double? iconButtonFactor; } diff --git a/lib/src/models/config/toolbar/buttons/link_style.dart b/lib/src/models/config/toolbar/buttons/link_style.dart index 7e2b6717..e82d750e 100644 --- a/lib/src/models/config/toolbar/buttons/link_style.dart +++ b/lib/src/models/config/toolbar/buttons/link_style.dart @@ -22,6 +22,7 @@ class QuillToolbarLinkStyleButtonOptions extends QuillToolbarBaseButtonOptions< this.linkDialogAction, this.dialogBarrierColor, this.iconSize, + this.iconButtonFactor, super.iconData, super.globalIconSize, super.afterButtonPressed, @@ -32,6 +33,7 @@ class QuillToolbarLinkStyleButtonOptions extends QuillToolbarBaseButtonOptions< }); final double? iconSize; + final double? iconButtonFactor; final QuillDialogTheme? dialogTheme; final RegExp? linkRegExp; final LinkDialogAction? linkDialogAction; diff --git a/lib/src/models/config/toolbar/buttons/link_style2.dart b/lib/src/models/config/toolbar/buttons/link_style2.dart new file mode 100644 index 00000000..75014143 --- /dev/null +++ b/lib/src/models/config/toolbar/buttons/link_style2.dart @@ -0,0 +1,65 @@ +import 'package:flutter/widgets.dart'; + +import '../../../themes/quill_dialog_theme.dart'; +import 'base.dart'; + +class QuillToolbarLinkStyleButton2ExtraOptions + extends QuillToolbarBaseButtonExtraOptions { + const QuillToolbarLinkStyleButton2ExtraOptions({ + required super.controller, + required super.context, + required super.onPressed, + }); +} + +class QuillToolbarLinkStyleButton2Options extends QuillToolbarBaseButtonOptions< + QuillToolbarLinkStyleButton2Options, + QuillToolbarLinkStyleButton2ExtraOptions> { + const QuillToolbarLinkStyleButton2Options({ + this.iconSize, + this.iconButtonFactor, + this.dialogTheme, + this.constraints, + this.addLinkLabel, + this.editLinkLabel, + this.linkColor, + this.validationMessage, + this.buttonSize, + this.dialogBarrierColor, + this.childrenSpacing = 16.0, + this.autovalidateMode = AutovalidateMode.disabled, + super.iconData, + super.afterButtonPressed, + super.tooltip, + super.iconTheme, + super.childBuilder, + super.controller, + }); + + final double? iconSize; + final double? iconButtonFactor; + final QuillDialogTheme? dialogTheme; + + /// The constrains for dialog. + final BoxConstraints? constraints; + + /// The text of label in link add mode. + final String? addLinkLabel; + + /// The text of label in link edit mode. + final String? editLinkLabel; + + /// The color of URL. + final Color? linkColor; + + /// The margin between child widgets in the dialog. + final double childrenSpacing; + + final AutovalidateMode autovalidateMode; + final String? validationMessage; + + /// The size of dialog buttons. + final Size? buttonSize; + + final Color? dialogBarrierColor; +} diff --git a/lib/src/models/config/toolbar/buttons/search.dart b/lib/src/models/config/toolbar/buttons/search.dart index b4505408..6d18fad2 100644 --- a/lib/src/models/config/toolbar/buttons/search.dart +++ b/lib/src/models/config/toolbar/buttons/search.dart @@ -21,6 +21,7 @@ class QuillToolbarSearchButtonOptions extends QuillToolbarBaseButtonOptions { super.iconTheme, this.dialogTheme, this.iconSize, + this.iconButtonFactor, this.dialogBarrierColor, this.fillColor, this.customOnPressedCallback, @@ -28,6 +29,7 @@ class QuillToolbarSearchButtonOptions extends QuillToolbarBaseButtonOptions { final QuillDialogTheme? dialogTheme; final double? iconSize; + final double? iconButtonFactor; /// By default will be [dialogBarrierColor] from [QuillSharedConfigurations] final Color? dialogBarrierColor; @@ -36,10 +38,10 @@ class QuillToolbarSearchButtonOptions extends QuillToolbarBaseButtonOptions { /// By default we will show simple search dialog ui /// you can pass value to this callback to change this - final QuillToolbarSearchButtomOnPressedCallback? customOnPressedCallback; + final QuillToolbarSearchButtonOnPressedCallback? customOnPressedCallback; } -typedef QuillToolbarSearchButtomOnPressedCallback = Future Function( +typedef QuillToolbarSearchButtonOnPressedCallback = Future Function( QuillController controller, ); diff --git a/lib/src/models/config/toolbar/buttons/select_alignment.dart b/lib/src/models/config/toolbar/buttons/select_alignment.dart index 3bbef892..0a90d350 100644 --- a/lib/src/models/config/toolbar/buttons/select_alignment.dart +++ b/lib/src/models/config/toolbar/buttons/select_alignment.dart @@ -18,12 +18,16 @@ class QuillToolbarSelectAlignmentButtonOptions this.iconsData, this.tooltips, this.iconSize, + this.iconButtonFactor, super.afterButtonPressed, + + /// This will called on every select alignment button super.childBuilder, super.controller, super.iconTheme, }); final double? iconSize; + final double? iconButtonFactor; /// Default to /// const QuillToolbarSelectAlignmentValues( diff --git a/lib/src/models/config/toolbar/buttons/select_header_style.dart b/lib/src/models/config/toolbar/buttons/select_header_style.dart index 1145afaf..6e70c9eb 100644 --- a/lib/src/models/config/toolbar/buttons/select_header_style.dart +++ b/lib/src/models/config/toolbar/buttons/select_header_style.dart @@ -20,22 +20,25 @@ class QuillToolbarSelectHeaderStyleButtonsOptions super.afterButtonPressed, super.childBuilder, super.controller, - super.iconData, super.iconTheme, super.tooltip, this.axis, - this.attributes = const [ - Attribute.header, - Attribute.h1, - Attribute.h2, - Attribute.h3, - ], + this.attributes, this.iconSize, + this.iconButtonFactor, }); - final List attributes; + /// Default value: + /// const [ + /// Attribute.header, + /// Attribute.h1, + /// Attribute.h2, + /// Attribute.h3, + /// ] + final List? attributes; /// By default we will the toolbar axis from [QuillToolbarConfigurations] final Axis? axis; final double? iconSize; + final double? iconButtonFactor; } diff --git a/lib/src/models/config/toolbar/buttons/toggle_check_list.dart b/lib/src/models/config/toolbar/buttons/toggle_check_list.dart index 7f4e98b1..54d456b9 100644 --- a/lib/src/models/config/toolbar/buttons/toggle_check_list.dart +++ b/lib/src/models/config/toolbar/buttons/toggle_check_list.dart @@ -22,6 +22,7 @@ class QuillToolbarToggleCheckListButtonOptions QuillToolbarToggleCheckListButtonExtraOptions> { const QuillToolbarToggleCheckListButtonOptions({ this.iconSize, + this.iconButtonFactor, this.fillColor, this.attribute = Attribute.unchecked, this.isShouldRequestKeyboard = false, @@ -34,6 +35,7 @@ class QuillToolbarToggleCheckListButtonOptions }); final double? iconSize; + final double? iconButtonFactor; final Color? fillColor; diff --git a/lib/src/models/config/toolbar/buttons/toggle_style.dart b/lib/src/models/config/toolbar/buttons/toggle_style.dart index 2ad9c1da..2c2ef1dd 100644 --- a/lib/src/models/config/toolbar/buttons/toggle_style.dart +++ b/lib/src/models/config/toolbar/buttons/toggle_style.dart @@ -22,6 +22,7 @@ class QuillToolbarToggleStyleButtonOptions const QuillToolbarToggleStyleButtonOptions({ super.iconData, this.iconSize, + this.iconButtonFactor, this.fillColor, super.tooltip, super.afterButtonPressed, @@ -31,5 +32,6 @@ class QuillToolbarToggleStyleButtonOptions }); final double? iconSize; + final double? iconButtonFactor; final Color? fillColor; } diff --git a/lib/src/models/config/toolbar/configurations.dart b/lib/src/models/config/toolbar/configurations.dart index 533ac8fb..63d1a3c2 100644 --- a/lib/src/models/config/toolbar/configurations.dart +++ b/lib/src/models/config/toolbar/configurations.dart @@ -1,16 +1,16 @@ import 'package:equatable/equatable.dart'; import 'package:flutter/foundation.dart' show immutable; import 'package:flutter/widgets.dart' - show Axis, Color, Decoration, WrapAlignment, WrapCrossAlignment; -import '../../../widgets/embeds.dart'; + show Axis, Color, Decoration, Widget, WrapAlignment, WrapCrossAlignment; +import '../../../widgets/embeds.dart'; import '../../structs/link_dialog_action.dart'; -import '../../themes/quill_custom_button.dart'; import '../../themes/quill_dialog_theme.dart'; import '../../themes/quill_icon_theme.dart'; import 'buttons/base.dart'; import 'buttons/clear_format.dart'; import 'buttons/color.dart'; +import 'buttons/custom_button.dart'; import 'buttons/font_family.dart'; import 'buttons/font_size.dart'; import 'buttons/history.dart'; @@ -26,6 +26,7 @@ export './../../../widgets/toolbar/buttons/search/search_dialog.dart'; export './buttons/base.dart'; export './buttons/clear_format.dart'; export './buttons/color.dart'; +export './buttons/custom_button.dart'; export './buttons/font_family.dart'; export './buttons/font_size.dart'; export './buttons/history.dart'; @@ -107,6 +108,7 @@ class QuillToolbarConfigurations extends Equatable { this.color, this.sectionDividerColor, this.sectionDividerSpace, + this.spacerWidget, /// By default it will calculated based on the [globalIconSize] from /// [base] in [QuillToolbarButtonOptions] @@ -130,6 +132,15 @@ class QuillToolbarConfigurations extends Equatable { /// If you want change spesefic buttons or all of them /// then you came to the right place final QuillToolbarButtonOptions buttonOptions; + + /// A widget that will placed between each button in the toolbar + /// can be used as a spacer + /// it will not used before the first button + /// it will not used after the last button + /// it will also not used in the toolbar dividers + /// Default value will be [SizedBox.shrink()] + /// some widgets like the header styles will be considered as one widget + final Widget? spacerWidget; final bool multiRowsDisplay; /// By default it will be @@ -143,7 +154,7 @@ class QuillToolbarConfigurations extends Equatable { /// 'Nunito': 'nunito', /// 'Pacifico': 'pacifico', /// 'Roboto Mono': 'roboto-mono', - /// 'Clear'.i18n: 'Clear' + /// 'Clear'.loc: 'Clear' /// }; /// ``` final Map? fontFamilyValues; @@ -154,7 +165,7 @@ class QuillToolbarConfigurations extends Equatable { /// 'Small'.i18n: 'small', /// 'Large'.i18n: 'large', /// 'Huge'.i18n: 'huge', - /// 'Clear'.i18n: '0' + /// 'Clear'.loc: '0' /// } /// ``` final Map? fontSizesValues; @@ -201,7 +212,7 @@ class QuillToolbarConfigurations extends Equatable { final bool showSearchButton; final bool showSubscript; final bool showSuperscript; - final List customButtons; + final List customButtons; /// The decoration to use for the toolbar. final Decoration? decoration; @@ -274,6 +285,7 @@ class QuillToolbarButtonOptions extends Equatable { this.selectHeaderStyleButtons = const QuillToolbarSelectHeaderStyleButtonsOptions(), this.linkStyle = const QuillToolbarLinkStyleButtonOptions(), + this.customButtons = const QuillToolbarCustomButtonOptions(), }); /// The base configurations for all the buttons which will apply to all @@ -319,6 +331,8 @@ class QuillToolbarButtonOptions extends Equatable { final QuillToolbarLinkStyleButtonOptions linkStyle; + final QuillToolbarCustomButtonOptions customButtons; + @override List get props => [ base, diff --git a/lib/src/models/documents/attribute.dart b/lib/src/models/documents/attribute.dart index 91e6c89e..fdfa9928 100644 --- a/lib/src/models/documents/attribute.dart +++ b/lib/src/models/documents/attribute.dart @@ -1,15 +1,17 @@ import 'dart:collection'; import 'package:equatable/equatable.dart'; +import 'package:meta/meta.dart' show immutable; import 'package:quiver/core.dart'; enum AttributeScope { - INLINE, // refer to https://quilljs.com/docs/formats/#inline - BLOCK, // refer to https://quilljs.com/docs/formats/#block - EMBEDS, // refer to https://quilljs.com/docs/formats/#embeds - IGNORE, // attributes that can be ignored + inline, // refer to https://quilljs.com/docs/formats/#inline + block, // refer to https://quilljs.com/docs/formats/#block + embeds, // refer to https://quilljs.com/docs/formats/#embeds + ignore, // attributes that can be ignored } +@immutable class Attribute extends Equatable { const Attribute( this.key, @@ -107,20 +109,22 @@ class Attribute extends Equatable { static final ScriptAttribute script = ScriptAttribute(null); - // TODO: You might want to mark those as key (like mobileWidthKey) - // because it was not very clear to a developer that is new to this project + @Deprecated('This property is no logner used in flutter_quill') static const String mobileWidth = 'mobileWidth'; + @Deprecated('This property is no logner used in flutter_quill') static const String mobileHeight = 'mobileHeight'; + @Deprecated('This property is no logner used in flutter_quill') static const String mobileMargin = 'mobileMargin'; + @Deprecated('This property is no logner used in flutter_quill') static const String mobileAlignment = 'mobileAlignment'; - /// For other platforms, for mobile use [mobileAlignment] + @Deprecated("Will be removed as it doesn't confirm to Quill js") static const String alignment = 'alignment'; - /// For other platforms, for mobile use [mobileMargin] + @Deprecated("Will be removed as it doesn't confirm to Quill js") static const String margin = 'margin'; static const ImageAttribute image = ImageAttribute(null); @@ -227,7 +231,7 @@ class Attribute extends Equatable { return IndentAttribute(level: level); } - bool get isInline => scope == AttributeScope.INLINE; + bool get isInline => scope == AttributeScope.inline; bool get isBlockExceptHeader => blockKeysExceptHeader.contains(key); @@ -283,110 +287,110 @@ class Attribute extends Equatable { } class BoldAttribute extends Attribute { - const BoldAttribute() : super('bold', AttributeScope.INLINE, true); + const BoldAttribute() : super('bold', AttributeScope.inline, true); } class ItalicAttribute extends Attribute { - const ItalicAttribute() : super('italic', AttributeScope.INLINE, true); + const ItalicAttribute() : super('italic', AttributeScope.inline, true); } class SmallAttribute extends Attribute { - const SmallAttribute() : super('small', AttributeScope.INLINE, true); + const SmallAttribute() : super('small', AttributeScope.inline, true); } class UnderlineAttribute extends Attribute { - const UnderlineAttribute() : super('underline', AttributeScope.INLINE, true); + const UnderlineAttribute() : super('underline', AttributeScope.inline, true); } class StrikeThroughAttribute extends Attribute { - const StrikeThroughAttribute() : super('strike', AttributeScope.INLINE, true); + const StrikeThroughAttribute() : super('strike', AttributeScope.inline, true); } class InlineCodeAttribute extends Attribute { - const InlineCodeAttribute() : super('code', AttributeScope.INLINE, true); + const InlineCodeAttribute() : super('code', AttributeScope.inline, true); } class FontAttribute extends Attribute { - const FontAttribute(String? val) : super('font', AttributeScope.INLINE, val); + const FontAttribute(String? val) : super('font', AttributeScope.inline, val); } class SizeAttribute extends Attribute { - const SizeAttribute(String? val) : super('size', AttributeScope.INLINE, val); + const SizeAttribute(String? val) : super('size', AttributeScope.inline, val); } class LinkAttribute extends Attribute { - const LinkAttribute(String? val) : super('link', AttributeScope.INLINE, val); + const LinkAttribute(String? val) : super('link', AttributeScope.inline, val); } class ColorAttribute extends Attribute { const ColorAttribute(String? val) - : super('color', AttributeScope.INLINE, val); + : super('color', AttributeScope.inline, val); } class BackgroundAttribute extends Attribute { const BackgroundAttribute(String? val) - : super('background', AttributeScope.INLINE, val); + : super('background', AttributeScope.inline, val); } /// This is custom attribute for hint class PlaceholderAttribute extends Attribute { const PlaceholderAttribute() - : super('placeholder', AttributeScope.INLINE, true); + : super('placeholder', AttributeScope.inline, true); } class HeaderAttribute extends Attribute { const HeaderAttribute({int? level}) - : super('header', AttributeScope.BLOCK, level); + : super('header', AttributeScope.block, level); } class IndentAttribute extends Attribute { const IndentAttribute({int? level}) - : super('indent', AttributeScope.BLOCK, level); + : super('indent', AttributeScope.block, level); } class AlignAttribute extends Attribute { - const AlignAttribute(String? val) : super('align', AttributeScope.BLOCK, val); + const AlignAttribute(String? val) : super('align', AttributeScope.block, val); } class ListAttribute extends Attribute { - const ListAttribute(String? val) : super('list', AttributeScope.BLOCK, val); + const ListAttribute(String? val) : super('list', AttributeScope.block, val); } class CodeBlockAttribute extends Attribute { - const CodeBlockAttribute() : super('code-block', AttributeScope.BLOCK, true); + const CodeBlockAttribute() : super('code-block', AttributeScope.block, true); } class BlockQuoteAttribute extends Attribute { - const BlockQuoteAttribute() : super('blockquote', AttributeScope.BLOCK, true); + const BlockQuoteAttribute() : super('blockquote', AttributeScope.block, true); } class DirectionAttribute extends Attribute { const DirectionAttribute(String? val) - : super('direction', AttributeScope.BLOCK, val); + : super('direction', AttributeScope.block, val); } class WidthAttribute extends Attribute { const WidthAttribute(String? val) - : super('width', AttributeScope.IGNORE, val); + : super('width', AttributeScope.ignore, val); } class HeightAttribute extends Attribute { const HeightAttribute(String? val) - : super('height', AttributeScope.IGNORE, val); + : super('height', AttributeScope.ignore, val); } class StyleAttribute extends Attribute { const StyleAttribute(String? val) - : super('style', AttributeScope.IGNORE, val); + : super('style', AttributeScope.ignore, val); } class TokenAttribute extends Attribute { - const TokenAttribute(String val) : super('token', AttributeScope.IGNORE, val); + const TokenAttribute(String val) : super('token', AttributeScope.ignore, val); } class ScriptAttribute extends Attribute { ScriptAttribute(ScriptAttributes? val) - : super('script', AttributeScope.INLINE, val?.value); + : super('script', AttributeScope.inline, val?.value); } enum ScriptAttributes { @@ -400,10 +404,10 @@ enum ScriptAttributes { class ImageAttribute extends Attribute { const ImageAttribute(String? url) - : super('image', AttributeScope.EMBEDS, url); + : super('image', AttributeScope.embeds, url); } class VideoAttribute extends Attribute { const VideoAttribute(String? url) - : super('video', AttributeScope.EMBEDS, url); + : super('video', AttributeScope.embeds, url); } diff --git a/lib/src/models/documents/document.dart b/lib/src/models/documents/document.dart index 55617865..4649765f 100644 --- a/lib/src/models/documents/document.dart +++ b/lib/src/models/documents/document.dart @@ -78,9 +78,9 @@ class Document { return Delta(); } - final delta = _rules.apply(RuleType.INSERT, this, index, + final delta = _rules.apply(RuleType.insert, this, index, data: data, len: replaceLength); - compose(delta, ChangeSource.LOCAL); + compose(delta, ChangeSource.local); return delta; } @@ -92,9 +92,9 @@ class Document { /// Returns an instance of [Delta] actually composed into this document. Delta delete(int index, int len) { assert(index >= 0 && len > 0); - final delta = _rules.apply(RuleType.DELETE, this, index, len: len); + final delta = _rules.apply(RuleType.delete, this, index, len: len); if (delta.isNotEmpty) { - compose(delta, ChangeSource.LOCAL); + compose(delta, ChangeSource.local); } return delta; } @@ -142,10 +142,10 @@ class Document { var delta = Delta(); - final formatDelta = _rules.apply(RuleType.FORMAT, this, index, + final formatDelta = _rules.apply(RuleType.format, this, index, len: len, attribute: attribute); if (formatDelta.isNotEmpty) { - compose(formatDelta, ChangeSource.LOCAL); + compose(formatDelta, ChangeSource.local); delta = delta.compose(formatDelta); } @@ -313,11 +313,11 @@ class Document { try { _delta = _delta.compose(delta); } catch (e) { - throw '_delta compose failed'; + throw StateError('_delta compose failed'); } if (_delta != _root.toDelta()) { - throw 'Compose failed'; + throw StateError('Compose failed'); } final change = DocChange(originalDelta, delta, changeSource); _observer.add(change); @@ -445,8 +445,8 @@ class Document { /// Source of a [Change]. enum ChangeSource { /// Change originated from a local action. Typically triggered by user. - LOCAL, + local, /// Change originated from a remote action. - REMOTE, + remote, } diff --git a/lib/src/models/documents/history.dart b/lib/src/models/documents/history.dart index fa87700c..638855f9 100644 --- a/lib/src/models/documents/history.dart +++ b/lib/src/models/documents/history.dart @@ -12,7 +12,7 @@ class History { this.lastRecorded = 0, }); - final HistoryStack stack = HistoryStack.empty(); + HistoryStack stack = HistoryStack.empty(); bool get hasUndo => stack.undo.isNotEmpty; @@ -34,7 +34,7 @@ class History { void handleDocChange(DocChange docChange) { if (ignoreChange) return; - if (!userOnly || docChange.source == ChangeSource.LOCAL) { + if (!userOnly || docChange.source == ChangeSource.local) { record(docChange.change, docChange.before); } else { transform(docChange.change); @@ -105,7 +105,7 @@ class History { dest.add(inverseDelta); lastRecorded = 0; ignoreChange = true; - doc.compose(delta, ChangeSource.LOCAL); + doc.compose(delta, ChangeSource.local); ignoreChange = false; return HistoryChanged(true, len); } @@ -124,8 +124,8 @@ class HistoryStack { : undo = [], redo = []; - final List undo; - final List redo; + List undo; + List redo; void clear() { undo.clear(); diff --git a/lib/src/models/documents/nodes/block.dart b/lib/src/models/documents/nodes/block.dart index ed5a4c75..886ad392 100644 --- a/lib/src/models/documents/nodes/block.dart +++ b/lib/src/models/documents/nodes/block.dart @@ -13,7 +13,7 @@ import 'node.dart'; /// - Text Alignment /// - Text Direction /// - Code Block -class Block extends Container { +base class Block extends Container { /// Creates new unmounted [Block]. @override Node newInstance() => Block(); diff --git a/lib/src/models/documents/nodes/container.dart b/lib/src/models/documents/nodes/container.dart index f061a4ca..605e472e 100644 --- a/lib/src/models/documents/nodes/container.dart +++ b/lib/src/models/documents/nodes/container.dart @@ -14,7 +14,7 @@ import 'node.dart'; /// /// Most of the operation handling logic is implemented by [Line] /// and [QuillText]. -abstract class Container extends Node { +abstract base class Container extends Node { final LinkedList _children = LinkedList(); /// List of children. @@ -137,17 +137,17 @@ abstract class Container extends Node { } @override - void retain(int index, int? length, Style? attributes) { + void retain(int index, int? len, Style? style) { assert(isNotEmpty); final child = queryChild(index, false); - child.node!.retain(child.offset, length, attributes); + child.node!.retain(child.offset, len, style); } @override - void delete(int index, int? length) { + void delete(int index, int? len) { assert(isNotEmpty); final child = queryChild(index, false); - child.node!.delete(child.offset, length); + child.node!.delete(child.offset, len); } @override diff --git a/lib/src/models/documents/nodes/embeddable.dart b/lib/src/models/documents/nodes/embeddable.dart index b2db0abc..24903315 100644 --- a/lib/src/models/documents/nodes/embeddable.dart +++ b/lib/src/models/documents/nodes/embeddable.dart @@ -1,4 +1,4 @@ -import 'dart:convert'; +import 'dart:convert' show jsonDecode, jsonEncode; /// An object which can be embedded into a Quill document. /// @@ -30,7 +30,7 @@ class Embeddable { /// the document model itself does not make any assumptions about the types /// of embedded objects and allows users to define their own types. class BlockEmbed extends Embeddable { - const BlockEmbed(String type, String data) : super(type, data); + const BlockEmbed(super.type, String super.data); static const String imageType = 'image'; static BlockEmbed image(String imageUrl) => BlockEmbed(imageType, imageUrl); @@ -47,7 +47,7 @@ class BlockEmbed extends Embeddable { } class CustomBlockEmbed extends BlockEmbed { - const CustomBlockEmbed(String type, String data) : super(type, data); + const CustomBlockEmbed(super.type, super.data); String toJsonString() => jsonEncode(toJson()); diff --git a/lib/src/models/documents/nodes/leaf.dart b/lib/src/models/documents/nodes/leaf.dart index 8bf78462..4f1fd3b0 100644 --- a/lib/src/models/documents/nodes/leaf.dart +++ b/lib/src/models/documents/nodes/leaf.dart @@ -8,7 +8,7 @@ import 'line.dart'; import 'node.dart'; /// A leaf in Quill document tree. -abstract class Leaf extends Node { +abstract base class Leaf extends Node { /// Creates a new [Leaf] with specified [data]. factory Leaf(Object data) { if (data is Embeddable) { @@ -216,10 +216,10 @@ abstract class Leaf extends Node { /// The reason we are renamed quill Text to [QuillText] so it doesn't /// conflict with the one from the widgets, material or cupertino library /// -class QuillText extends Leaf { - QuillText([String text = '']) +base class QuillText extends Leaf { + QuillText([String super.text = '']) : assert(!text.contains('\n')), - super.val(text); + super.val(); @override Node newInstance() => QuillText(value); @@ -249,8 +249,8 @@ class QuillText extends Leaf { /// necessarily mean the embed will look according to that style. For instance, /// applying "bold" style to an image gives no effect, while adding a "link" to /// an image actually makes the image react to user's action. -class Embed extends Leaf { - Embed(Embeddable data) : super.val(data); +base class Embed extends Leaf { + Embed(Embeddable super.data) : super.val(); // Refer to https://www.fileformat.info/info/unicode/char/fffc/index.htm static const kObjectReplacementCharacter = '\uFFFC'; diff --git a/lib/src/models/documents/nodes/line.dart b/lib/src/models/documents/nodes/line.dart index 58ba9cc6..e11f095c 100644 --- a/lib/src/models/documents/nodes/line.dart +++ b/lib/src/models/documents/nodes/line.dart @@ -19,7 +19,7 @@ import 'node.dart'; /// /// When a line contains an embed, it fully occupies the line, no other embeds /// or text nodes are allowed. -class Line extends Container { +base class Line extends Container { @override Leaf get defaultChild => QuillText(); @@ -136,15 +136,15 @@ class Line extends Container { if (isLineFormat) { assert( style.values.every((attr) => - attr.scope == AttributeScope.BLOCK || - attr.scope == AttributeScope.IGNORE), + attr.scope == AttributeScope.block || + attr.scope == AttributeScope.ignore), 'It is not allowed to apply inline attributes to line itself.'); _format(style); } else { // Otherwise forward to children as it's an inline format update. assert(style.values.every((attr) => - attr.scope == AttributeScope.INLINE || - attr.scope == AttributeScope.IGNORE)); + attr.scope == AttributeScope.inline || + attr.scope == AttributeScope.ignore)); assert(index + local != thisLength); super.retain(index, local, style); } @@ -351,7 +351,7 @@ class Line extends Container { var result = const Style(); final excluded = {}; - void _handle(Style style) { + void handle(Style style) { for (final attr in result.values) { if (!style.containsKey(attr.key) || (style.attributes[attr.key] != attr.value)) { @@ -368,7 +368,7 @@ class Line extends Container { var pos = node.length - data.offset; while (!node!.isLast && pos < local) { node = node.next as Leaf; - _handle(node.style); + handle(node.style); pos += node.length; } } @@ -382,7 +382,7 @@ class Line extends Container { final remaining = len - local; if (remaining > 0 && nextLine != null) { final rest = nextLine!.collectStyle(0, remaining); - _handle(rest); + handle(rest); } return result; @@ -521,35 +521,35 @@ class Line extends Container { } int _getPlainText(int offset, int len, StringBuffer plainText) { - var _len = len; + var len0 = len; final data = queryChild(offset, false); var node = data.node as Leaf?; - while (_len > 0) { + while (len0 > 0) { if (node == null) { // blank line plainText.write('\n'); - _len -= 1; + len0 -= 1; } else { - _len = _getNodeText(node, plainText, offset - node.offset, _len); + len0 = _getNodeText(node, plainText, offset - node.offset, len0); - while (!node!.isLast && _len > 0) { + while (!node!.isLast && len0 > 0) { node = node.next as Leaf; - _len = _getNodeText(node, plainText, 0, _len); + len0 = _getNodeText(node, plainText, 0, len0); } - if (_len > 0) { + if (len0 > 0) { // end of this line plainText.write('\n'); - _len -= 1; + len0 -= 1; } } - if (_len > 0 && nextLine != null) { - _len = nextLine!._getPlainText(0, _len, plainText); + if (len0 > 0 && nextLine != null) { + len0 = nextLine!._getPlainText(0, len0, plainText); } } - return _len; + return len0; } } diff --git a/lib/src/models/documents/nodes/node.dart b/lib/src/models/documents/nodes/node.dart index 098f1241..c196b47f 100644 --- a/lib/src/models/documents/nodes/node.dart +++ b/lib/src/models/documents/nodes/node.dart @@ -17,10 +17,24 @@ import 'line.dart'; /// /// The current parent node is exposed by the [parent] property. A node is /// considered [mounted] when the [parent] property is not `null`. -abstract class Node extends LinkedListEntry { +abstract base class Node extends LinkedListEntry { /// Current parent of this node. May be null if this node is not mounted. Container? parent; + /// The style attributes + /// Note: This is not the same as style attribute of css + /// + /// Example: + /// + /// { + /// "insert": { + /// "image": "https://user-images.githubusercontent.com/122956/72955931-ccc07900-3d52-11ea-89b1-d468a6e2aa2b.png" + /// }, + /// "attributes": { // this one + /// "width": "230", + /// "style": "display: block; margin: auto; width: 500px;" // Not this + /// } + /// }, Style get style => _style; Style _style = const Style(); @@ -127,7 +141,7 @@ abstract class Node extends LinkedListEntry { } /// Root node of document tree. -class Root extends Container> { +base class Root extends Container> { @override Node newInstance() => Root(); diff --git a/lib/src/models/documents/style.dart b/lib/src/models/documents/style.dart index d3e1247a..589bfc15 100644 --- a/lib/src/models/documents/style.dart +++ b/lib/src/models/documents/style.dart @@ -21,7 +21,7 @@ class Style { final result = attributes.map((key, dynamic value) { final attr = Attribute.fromKeyValue(key, value); return MapEntry( - key, attr ?? Attribute(key, AttributeScope.IGNORE, value)); + key, attr ?? Attribute(key, AttributeScope.ignore, value)); }); return Style.attr(result); } @@ -45,10 +45,10 @@ class Style { bool get isInline => isNotEmpty && values.every((item) => item.isInline); bool get isBlock => - isNotEmpty && values.every((item) => item.scope == AttributeScope.BLOCK); + isNotEmpty && values.every((item) => item.scope == AttributeScope.block); bool get isIgnored => - isNotEmpty && values.every((item) => item.scope == AttributeScope.IGNORE); + isNotEmpty && values.every((item) => item.scope == AttributeScope.ignore); Attribute get single => _attributes.values.single; diff --git a/lib/src/models/quill_delta.dart b/lib/src/models/quill_delta.dart index a9babf60..196651e9 100644 --- a/lib/src/models/quill_delta.dart +++ b/lib/src/models/quill_delta.dart @@ -1,4 +1,6 @@ /// Implementation of Quill Delta format in Dart. +library; + import 'dart:math' as math; import 'package:collection/collection.dart'; @@ -262,11 +264,11 @@ class Delta { b ??= const {}; final attributes = {}; - (a.keys.toList()..addAll(b.keys)).forEach((key) { - if (a![key] != b![key]) { + for (final key in (a.keys.toList()..addAll(b.keys))) { + if (a[key] != b[key]) { attributes[key] = b.containsKey(key) ? b[key] : null; } - }); + } return attributes.keys.isNotEmpty ? attributes : null; } @@ -514,7 +516,7 @@ class Delta { final thisIter = DeltaIterator(this); final otherIter = DeltaIterator(other); - diffResult.forEach((component) { + for (final component in diffResult) { var length = component.text.length; while (length > 0) { var opLength = 0; @@ -549,7 +551,7 @@ class Delta { } length -= opLength; } - }); + } return retDelta..trim(); } @@ -787,9 +789,9 @@ class DeltaIterator { final op = delta.elementAt(_index); final opKey = op.key; final opAttributes = op.attributes; - final _currentOffset = _offset; - final actualLength = math.min(op.length! - _currentOffset, length); - if (actualLength == op.length! - _currentOffset) { + final currentOffset = _offset; + final actualLength = math.min(op.length! - currentOffset, length); + if (actualLength == op.length! - currentOffset) { _index++; _offset = 0; } else { @@ -797,7 +799,7 @@ class DeltaIterator { } final opData = op.isInsert && op.data is String ? (op.data as String) - .substring(_currentOffset, _currentOffset + actualLength) + .substring(currentOffset, currentOffset + actualLength) : op.data; final opIsNotEmpty = opData is String ? opData.isNotEmpty : true; // embeds are never empty diff --git a/lib/src/models/rules/delete.dart b/lib/src/models/rules/delete.dart index fd547825..9757bc45 100644 --- a/lib/src/models/rules/delete.dart +++ b/lib/src/models/rules/delete.dart @@ -1,14 +1,17 @@ +import 'package:meta/meta.dart' show immutable; + import '../documents/attribute.dart'; import '../documents/nodes/embeddable.dart'; import '../quill_delta.dart'; import 'rule.dart'; /// A heuristic rule for delete operations. +@immutable abstract class DeleteRule extends Rule { const DeleteRule(); @override - RuleType get type => RuleType.DELETE; + RuleType get type => RuleType.delete; @override void validateArgs(int? len, Object? data, Attribute? attribute) { @@ -18,6 +21,7 @@ abstract class DeleteRule extends Rule { } } +@immutable class EnsureLastLineBreakDeleteRule extends DeleteRule { const EnsureLastLineBreakDeleteRule(); @@ -34,6 +38,7 @@ class EnsureLastLineBreakDeleteRule extends DeleteRule { /// Fallback rule for delete operations which simply deletes specified text /// range without any special handling. +@immutable class CatchAllDeleteRule extends DeleteRule { const CatchAllDeleteRule(); @@ -54,6 +59,7 @@ class CatchAllDeleteRule extends DeleteRule { /// This rule makes sure to apply all style attributes of deleted newline /// to the next available newline, which may reset any style attributes /// already present there. +@immutable class PreserveLineStyleOnMergeRule extends DeleteRule { const PreserveLineStyleOnMergeRule(); @@ -112,6 +118,7 @@ class PreserveLineStyleOnMergeRule extends DeleteRule { /// Prevents user from merging a line containing an embed with other lines. /// This rule applies to video, not image. /// The rule relates to [InsertEmbedsRule]. +@immutable class EnsureEmbedLineRule extends DeleteRule { const EnsureEmbedLineRule(); diff --git a/lib/src/models/rules/format.dart b/lib/src/models/rules/format.dart index a57bac7a..ebe76418 100644 --- a/lib/src/models/rules/format.dart +++ b/lib/src/models/rules/format.dart @@ -1,13 +1,16 @@ +import 'package:meta/meta.dart' show immutable; + import '../documents/attribute.dart'; import '../quill_delta.dart'; import 'rule.dart'; /// A heuristic rule for format (retain) operations. +@immutable abstract class FormatRule extends Rule { const FormatRule(); @override - RuleType get type => RuleType.FORMAT; + RuleType get type => RuleType.format; @override void validateArgs(int? len, Object? data, Attribute? attribute) { @@ -19,6 +22,7 @@ abstract class FormatRule extends Rule { /// Produces Delta with line-level attributes applied strictly to /// newline characters. +@immutable class ResolveLineFormatRule extends FormatRule { const ResolveLineFormatRule(); @@ -30,7 +34,7 @@ class ResolveLineFormatRule extends FormatRule { Object? data, Attribute? attribute, }) { - if (attribute!.scope != AttributeScope.BLOCK) { + if (attribute!.scope != AttributeScope.block) { return null; } @@ -109,6 +113,7 @@ class ResolveLineFormatRule extends FormatRule { } /// Allows updating link format with collapsed selection. +@immutable class FormatLinkAtCaretPositionRule extends FormatRule { const FormatLinkAtCaretPositionRule(); @@ -148,6 +153,7 @@ class FormatLinkAtCaretPositionRule extends FormatRule { /// Produces Delta with inline-level attributes applied to all characters /// except newlines. +@immutable class ResolveInlineFormatRule extends FormatRule { const ResolveInlineFormatRule(); @@ -159,7 +165,7 @@ class ResolveInlineFormatRule extends FormatRule { Object? data, Attribute? attribute, }) { - if (attribute!.scope != AttributeScope.INLINE) { + if (attribute!.scope != AttributeScope.inline) { return null; } @@ -193,6 +199,7 @@ class ResolveInlineFormatRule extends FormatRule { } /// Produces Delta with attributes applied to image leaf node +@immutable class ResolveImageFormatRule extends FormatRule { const ResolveImageFormatRule(); diff --git a/lib/src/models/rules/insert.dart b/lib/src/models/rules/insert.dart index 92fc461e..daf3c110 100644 --- a/lib/src/models/rules/insert.dart +++ b/lib/src/models/rules/insert.dart @@ -1,3 +1,5 @@ +import 'package:meta/meta.dart' show immutable; + import '../../models/documents/document.dart'; import '../documents/attribute.dart'; import '../documents/nodes/embeddable.dart'; @@ -6,11 +8,12 @@ import '../quill_delta.dart'; import 'rule.dart'; /// A heuristic rule for insert operations. +@immutable abstract class InsertRule extends Rule { const InsertRule(); @override - RuleType get type => RuleType.INSERT; + RuleType get type => RuleType.insert; @override void validateArgs(int? len, Object? data, Attribute? attribute) { @@ -23,6 +26,7 @@ abstract class InsertRule extends Rule { /// /// This rule ignores scenarios when the line is split on its edge, meaning /// a newline is inserted at the beginning or the end of a line. +@immutable class PreserveLineStyleOnSplitRule extends InsertRule { const PreserveLineStyleOnSplitRule(); @@ -73,6 +77,7 @@ class PreserveLineStyleOnSplitRule extends InsertRule { /// * pasting text containing multiple lines of text in a block /// /// This rule may also be activated for changes triggered by auto-correct. +@immutable class PreserveBlockStyleOnInsertRule extends InsertRule { const PreserveBlockStyleOnInsertRule(); @@ -149,6 +154,7 @@ class PreserveBlockStyleOnInsertRule extends InsertRule { /// This rule is only applied when the cursor is on the last line of a block. /// When the cursor is in the middle of a block we allow adding empty lines /// and preserving the block's style. +@immutable class AutoExitBlockRule extends InsertRule { const AutoExitBlockRule(); @@ -228,6 +234,7 @@ class AutoExitBlockRule extends InsertRule { /// /// This handles scenarios when a new line is added when at the end of a /// heading line. The newly added line should be a regular paragraph. +@immutable class ResetLineFormatOnNewLineRule extends InsertRule { const ResetLineFormatOnNewLineRule(); @@ -264,6 +271,7 @@ class ResetLineFormatOnNewLineRule extends InsertRule { /// Handles all format operations which manipulate embeds. /// This rule wraps line breaks around video, not image. +@immutable class InsertEmbedsRule extends InsertRule { const InsertEmbedsRule(); @@ -327,6 +335,7 @@ class InsertEmbedsRule extends InsertRule { /// the URL pattern. /// /// The link attribute is applied as the user types. +@immutable class AutoFormatMultipleLinksRule extends InsertRule { const AutoFormatMultipleLinksRule(); @@ -355,17 +364,14 @@ class AutoFormatMultipleLinksRule extends InsertRule { // https://example.net/ // URL generator tool (https://www.randomlists.com/urls) is used. - // TODO: You might want to rename those but everywhere even in - // flutter_quill_extensions - - static const _oneLinePattern = + static const _oneLineLinkPattern = r'^https?:\/\/[\w\-]+(\.[\w\-]+)*(:\d+)?(\/.*)?$'; static const _detectLinkPattern = r'https?:\/\/[\w\-]+(\.[\w\-]+)*(:\d+)?(\/[^\s]*)?'; /// It requires a valid link in one link - static final oneLineRegExp = RegExp( - _oneLinePattern, + static final oneLineLinkRegExp = RegExp( + _oneLineLinkPattern, caseSensitive: false, ); @@ -375,10 +381,7 @@ class AutoFormatMultipleLinksRule extends InsertRule { _detectLinkPattern, caseSensitive: false, ); - @Deprecated( - 'Please use [linkRegExp1] or [linkRegExp2]', - ) - static final linkRegExp = oneLineRegExp; + static final linkRegExp = oneLineLinkRegExp; @override Delta? applyRule( @@ -492,6 +495,7 @@ class AutoFormatMultipleLinksRule extends InsertRule { /// Applies link format to text segment (which looks like a link) when user /// inserts space character after it. +@immutable class AutoFormatLinksRule extends InsertRule { const AutoFormatLinksRule(); @@ -516,6 +520,7 @@ class AutoFormatLinksRule extends InsertRule { try { final cand = (prev.data as String).split('\n').last.split(' ').last; final link = Uri.parse(cand); + // TODO: Can be improved a little if (!['https', 'http'].contains(link.scheme)) { return null; } @@ -537,6 +542,7 @@ class AutoFormatLinksRule extends InsertRule { } /// Preserves inline styles when user inserts text inside formatted segment. +@immutable class PreserveInlineStylesRule extends InsertRule { const PreserveInlineStylesRule(); @@ -588,6 +594,7 @@ class PreserveInlineStylesRule extends InsertRule { } /// Fallback rule which simply inserts text as-is without any special handling. +@immutable class CatchAllInsertRule extends InsertRule { const CatchAllInsertRule(); @@ -618,6 +625,7 @@ _NextNewLine _getNextNewLine(DeltaIterator iterator) { return const _NextNewLine(null, null); } +@immutable class _NextNewLine { const _NextNewLine(this.operation, this.skipped); diff --git a/lib/src/models/rules/rule.dart b/lib/src/models/rules/rule.dart index 45b8bdf8..976fda43 100644 --- a/lib/src/models/rules/rule.dart +++ b/lib/src/models/rules/rule.dart @@ -1,3 +1,5 @@ +import 'package:meta/meta.dart' show immutable; + import '../documents/attribute.dart'; import '../documents/document.dart'; import '../quill_delta.dart'; @@ -5,8 +7,9 @@ import 'delete.dart'; import 'format.dart'; import 'insert.dart'; -enum RuleType { INSERT, DELETE, FORMAT } +enum RuleType { insert, delete, format } +@immutable abstract class Rule { const Rule(); @@ -48,24 +51,24 @@ class Rules { List _customRules = []; final List _rules; - static final Rules _instance = Rules([ - const FormatLinkAtCaretPositionRule(), - const ResolveLineFormatRule(), - const ResolveInlineFormatRule(), - const ResolveImageFormatRule(), - const InsertEmbedsRule(), - const AutoExitBlockRule(), - const PreserveBlockStyleOnInsertRule(), - const PreserveLineStyleOnSplitRule(), - const ResetLineFormatOnNewLineRule(), - const AutoFormatLinksRule(), - const AutoFormatMultipleLinksRule(), - const PreserveInlineStylesRule(), - const CatchAllInsertRule(), - const EnsureEmbedLineRule(), - const PreserveLineStyleOnMergeRule(), - const CatchAllDeleteRule(), - const EnsureLastLineBreakDeleteRule() + static final Rules _instance = Rules(const [ + FormatLinkAtCaretPositionRule(), + ResolveLineFormatRule(), + ResolveInlineFormatRule(), + ResolveImageFormatRule(), + InsertEmbedsRule(), + AutoExitBlockRule(), + PreserveBlockStyleOnInsertRule(), + PreserveLineStyleOnSplitRule(), + ResetLineFormatOnNewLineRule(), + AutoFormatLinksRule(), + AutoFormatMultipleLinksRule(), + PreserveInlineStylesRule(), + CatchAllInsertRule(), + EnsureEmbedLineRule(), + PreserveLineStyleOnMergeRule(), + CatchAllDeleteRule(), + EnsureLastLineBreakDeleteRule() ]); static Rules getInstance() => _instance; diff --git a/lib/src/models/structs/doc_change.dart b/lib/src/models/structs/doc_change.dart index d2772a59..33e398ec 100644 --- a/lib/src/models/structs/doc_change.dart +++ b/lib/src/models/structs/doc_change.dart @@ -1,8 +1,11 @@ +import 'package:meta/meta.dart' show immutable; + import '../documents/document.dart'; import '../quill_delta.dart'; +@immutable class DocChange { - DocChange( + const DocChange( this.before, this.change, this.source, diff --git a/lib/src/models/structs/history_changed.dart b/lib/src/models/structs/history_changed.dart index abb61567..1a8b2d60 100644 --- a/lib/src/models/structs/history_changed.dart +++ b/lib/src/models/structs/history_changed.dart @@ -1,3 +1,6 @@ +import 'package:meta/meta.dart' show immutable; + +@immutable class HistoryChanged { const HistoryChanged( this.changed, diff --git a/lib/src/models/structs/image_url.dart b/lib/src/models/structs/image_url.dart index 097e199b..cc86005e 100644 --- a/lib/src/models/structs/image_url.dart +++ b/lib/src/models/structs/image_url.dart @@ -1,3 +1,6 @@ +import 'package:meta/meta.dart' show immutable; + +@immutable class ImageUrl { const ImageUrl( this.url, diff --git a/lib/src/models/structs/link_dialog_action.dart b/lib/src/models/structs/link_dialog_action.dart index 06288c9f..08a457ab 100644 --- a/lib/src/models/structs/link_dialog_action.dart +++ b/lib/src/models/structs/link_dialog_action.dart @@ -1,7 +1,9 @@ -import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart' show Widget; +import 'package:meta/meta.dart' show immutable; +@immutable class LinkDialogAction { - LinkDialogAction({required this.builder}); + const LinkDialogAction({required this.builder}); - Widget Function(bool canPress, void Function() applyLink) builder; + final Widget Function(bool canPress, void Function() applyLink) builder; } diff --git a/lib/src/models/structs/offset_value.dart b/lib/src/models/structs/offset_value.dart index 58275458..49cbc70f 100644 --- a/lib/src/models/structs/offset_value.dart +++ b/lib/src/models/structs/offset_value.dart @@ -1,5 +1,8 @@ +import 'package:meta/meta.dart' show immutable; + +@immutable class OffsetValue { - OffsetValue(this.offset, this.value, [this.length]); + const OffsetValue(this.offset, this.value, [this.length]); final int offset; final int? length; final T value; diff --git a/lib/src/models/structs/optional_size.dart b/lib/src/models/structs/optional_size.dart index a887b468..3e33bbfe 100644 --- a/lib/src/models/structs/optional_size.dart +++ b/lib/src/models/structs/optional_size.dart @@ -1,5 +1,8 @@ +import 'package:meta/meta.dart' show immutable; + +@immutable class OptionalSize { - OptionalSize( + const OptionalSize( this.width, this.height, ); @@ -11,4 +14,14 @@ class OptionalSize { /// If non-null, requires the child to have exactly this height. /// If null, the child is free to choose its own height. final double? height; + + OptionalSize copyWith({ + double? width, + double? height, + }) { + return OptionalSize( + width ?? this.width, + height ?? this.height, + ); + } } diff --git a/lib/src/models/structs/segment_leaf_node.dart b/lib/src/models/structs/segment_leaf_node.dart index 43921b93..880dd912 100644 --- a/lib/src/models/structs/segment_leaf_node.dart +++ b/lib/src/models/structs/segment_leaf_node.dart @@ -1,6 +1,9 @@ +import 'package:meta/meta.dart' show immutable; + import '../documents/nodes/leaf.dart'; import '../documents/nodes/line.dart'; +@immutable class SegmentLeafNode { const SegmentLeafNode(this.line, this.leaf); diff --git a/lib/src/models/structs/vertical_spacing.dart b/lib/src/models/structs/vertical_spacing.dart index 54f76f7c..d19a2f1c 100644 --- a/lib/src/models/structs/vertical_spacing.dart +++ b/lib/src/models/structs/vertical_spacing.dart @@ -1,3 +1,6 @@ +import 'package:meta/meta.dart' show immutable; + +@immutable class VerticalSpacing { const VerticalSpacing( this.top, diff --git a/lib/src/models/themes/quill_custom_button.dart b/lib/src/models/themes/quill_custom_button.dart deleted file mode 100644 index da1eb06d..00000000 --- a/lib/src/models/themes/quill_custom_button.dart +++ /dev/null @@ -1,26 +0,0 @@ -import 'package:flutter/material.dart'; - -import '../../widgets/toolbar/base_toolbar.dart'; - -class QuillCustomButton extends QuillToolbarBaseButtonOptions { - const QuillCustomButton({ - super.iconData, - this.iconColor, - this.onTap, - super.tooltip, - this.iconSize, - this.child, - super.iconTheme, - }); - - ///The icon color; - final Color? iconColor; - - ///The function when the icon is tapped - final VoidCallback? onTap; - - ///The customButton placeholder - final Widget? child; - - final double? iconSize; -} diff --git a/lib/src/models/themes/quill_dialog_theme.dart b/lib/src/models/themes/quill_dialog_theme.dart index 3664dc3e..f93f2564 100644 --- a/lib/src/models/themes/quill_dialog_theme.dart +++ b/lib/src/models/themes/quill_dialog_theme.dart @@ -1,7 +1,18 @@ -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; +import 'package:flutter/foundation.dart' show Diagnosticable; +import 'package:flutter/material.dart' + show + BoxConstraints, + ButtonStyle, + Color, + EdgeInsets, + EdgeInsetsGeometry, + ShapeBorder, + TextStyle; +import 'package:meta/meta.dart' show immutable; /// Used to configure the dialog's look and feel. + +@immutable class QuillDialogTheme with Diagnosticable { const QuillDialogTheme({ this.buttonTextStyle, diff --git a/lib/src/models/themes/quill_icon_theme.dart b/lib/src/models/themes/quill_icon_theme.dart index 1ef3fdd8..ca70b800 100644 --- a/lib/src/models/themes/quill_icon_theme.dart +++ b/lib/src/models/themes/quill_icon_theme.dart @@ -1,5 +1,8 @@ -import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart' show Color; +import 'package:meta/meta.dart' show immutable; + +@immutable class QuillIconTheme { const QuillIconTheme( {this.iconSelectedColor, diff --git a/lib/src/translations/toolbar.i18n.dart b/lib/src/translations/toolbar.i18n.dart deleted file mode 100644 index d9957111..00000000 --- a/lib/src/translations/toolbar.i18n.dart +++ /dev/null @@ -1,2284 +0,0 @@ -import 'package:i18n_extension/i18n_extension.dart'; - -// TODO: The translation need to be changed and re-reviewd - -extension Localization on String { - static final _t = Translations.byLocale('en') + - { - 'en': { - 'Paste a link': 'Paste a link', - 'Ok': 'Ok', - 'Select Color': 'Select Color', - 'Gallery': 'Gallery', - 'Link': 'Link', - 'Open': 'Open', - 'Copy': 'Copy', - 'Remove': 'Remove', - 'Save': 'Save', - 'Zoom': 'Zoom', - 'Saved': 'Saved', - 'Text': 'Text', - 'Resize': 'Resize', - 'Width': 'Width', - 'Height': 'Height', - 'Size': 'Size', - 'Small': 'Small', - 'Large': 'Large', - 'Huge': 'Huge', - 'Clear': 'Clear', - 'Font': 'Font', - 'Search': 'Search', - 'Camera': 'Camera', - 'Video': 'Video', - 'Undo': 'Undo', - 'Redo': 'Redo', - 'Font family': 'Font family', - 'Font size': 'Font size', - 'Bold': 'Bold', - 'Subscript': 'Subscript', - 'Superscript': 'Superscript', - 'Italic': 'Italic', - 'Underline': 'Underline', - 'Strike through': 'Strike through', - 'Inline code': 'Inline code', - 'Font color': 'Font color', - 'Background color': 'Background color', - 'Clear format': 'Clear format', - 'Align left': 'Align left', - 'Align center': 'Align center', - 'Align right': 'Align right', - 'Justify win width': 'Justify win width', - 'Text direction': 'Text direction', - 'Header style': 'Header style', - 'Numbered list': 'Numbered list', - 'Bullet list': 'Bullet list', - 'Checked list': 'Checked list', - 'Code block': 'Code block', - 'Quote': 'Quote', - 'Increase indent': 'Increase indent', - 'Decrease indent': 'Decrease indent', - 'Insert URL': 'Insert URL', - 'Visit link': 'Visit link', - 'Enter link': 'Enter link', - 'Enter media': 'Enter media', - 'Edit': 'Edit', - 'Apply': 'Apply', - 'Hex': 'Hex', - 'Material': 'Material', - 'Color': 'Color', - 'Find text': 'Find text', - 'Move to previous occurrence': 'Move to previous occurrence', - 'Move to next occurrence': 'Move to next occurrence', - 'Saved using the network': 'Saved using the network', - 'Saved using the local storage': 'Saved using the local storage', - 'Error while saving image': 'Error while saving image', - 'Please enter a text for your link': "e.g., 'Learn more)", - 'Please enter the link url': "e.g., 'https://example.com'", - 'Please enter a valid image url': 'Please enter a valid image url' - }, - 'en_us': { - 'Paste a link': 'Paste a link', - 'Ok': 'Ok', - 'Select Color': 'Select Color', - 'Gallery': 'Gallery', - 'Link': 'Link', - 'Open': 'Open', - 'Copy': 'Copy', - 'Remove': 'Remove', - 'Save': 'Save', - 'Zoom': 'Zoom', - 'Saved': 'Saved', - 'Text': 'Text', - 'Resize': 'Resize', - 'Width': 'Width', - 'Height': 'Height', - 'Size': 'Size', - 'Small': 'Small', - 'Large': 'Large', - 'Huge': 'Huge', - 'Clear': 'Clear', - 'Font': 'Font', - 'Search': 'Search', - 'Camera': 'Camera', - 'Video': 'Video', - 'Undo': 'Undo', - 'Redo': 'Redo', - 'Font family': 'Font family', - 'Font size': 'Font size', - 'Bold': 'Bold', - 'Subscript': 'Subscript', - 'Superscript': 'Superscript', - 'Italic': 'Italic', - 'Underline': 'Underline', - 'Strike through': 'Strike through', - 'Inline code': 'Inline code', - 'Font color': 'Font color', - 'Background color': 'Background color', - 'Clear format': 'Clear format', - 'Align left': 'Align left', - 'Align center': 'Align center', - 'Align right': 'Align right', - 'Justify win width': 'Justify win width', - 'Text direction': 'Text direction', - 'Header style': 'Header style', - 'Numbered list': 'Numbered list', - 'Bullet list': 'Bullet list', - 'Checked list': 'Checked list', - 'Code block': 'Code block', - 'Quote': 'Quote', - 'Increase indent': 'Increase indent', - 'Decrease indent': 'Decrease indent', - 'Insert URL': 'Insert URL', - 'Visit link': 'Visit link', - 'Enter link': 'Enter link', - 'Enter media': 'Enter media', - 'Edit': 'Edit', - 'Apply': 'Apply', - 'Hex': 'Hex', - 'Material': 'Material', - 'Color': 'Color', - 'Find text': 'Find text', - 'Move to previous occurrence': 'Move to previous occurrence', - 'Move to next occurrence': 'Move to next occurrence', - 'Saved using the network': 'Saved using the network', - 'Saved using the local storage': 'Saved using the local storage', - 'Error while saving image': 'Error while saving image', - 'Please enter a text for your link': "e.g., 'Learn more)", - 'Please enter the link url': "e.g., 'https://example.com'", - 'Please enter a valid image url': 'Please enter a valid image url' - }, - 'ar': { - 'Paste a link': 'نسخ الرابط', - 'Ok': 'نعم', - 'Select Color': 'اختار اللون', - 'Gallery': 'المعرض', - 'Link': 'الرابط', - 'Open': 'فتح', - 'Copy': 'نسخ', - 'Remove': 'إزالة', - 'Save': 'حفظ', - 'Zoom': 'تكبير', - 'Saved': 'تم الحفظ', - 'Text': 'نص', - 'Resize': 'تحجيم', - 'Width': 'عرض', - 'Height': 'ارتفاع', - 'Size': 'حجم', - 'Small': 'صغير', - 'Large': 'كبير', - 'Huge': 'ضخم', - 'Clear': 'تنظيف', - 'Font': 'خط', - 'Search': 'بحث', - - 'Camera': 'كاميرا', - 'Video': 'فيديو', - 'Undo': 'تراجع', - 'Redo': 'تقدم', - 'Font family': 'عائلة الخط', - 'Font size': 'حجم الخط', - 'Bold': 'عريض', - 'Subscript': 'نص سفلي', - 'Superscript': 'نص علوي', - 'Italic': 'مائل', - 'Underline': 'تحته خط', - 'Strike through': 'داخله خط', - 'Inline code': 'كود بوسط السطر', - 'Font color': 'لون الخط', - 'Background color': 'لون الخلفية', - 'Clear format': 'تنظيف التنسيق', - 'Align left': 'محاذاة اليسار', - 'Align center': 'محاذاة الوسط', - 'Align right': 'محاذاة اليمين', - // i think it should be 'Justify with width' - // it is wrong in all properties - 'Justify win width': 'تبرير مع العرض', - 'Text direction': 'اتجاه النص', - 'Header style': 'ستايل العنوان', - 'Numbered list': 'قائمة مرقمة', - 'Bullet list': 'قائمة منقطة', - 'Checked list': 'قائمة للمهام', - 'Code block': 'كود كامل', - 'Quote': 'اقتباس', - 'Increase indent': 'زيادة الهامش', - 'Decrease indent': 'تنقيص الهامش', - 'Insert URL': 'ادخل عنوان رابط', - 'Visit link': 'زيارة الرابط', - 'Enter link': 'ادخل رابط', - 'Enter media': 'ادخل وسائط', - 'Edit': 'تعديل', - 'Apply': 'تطبيق', - 'Hex': 'Hex', - 'Material': 'Material', - 'Color': 'اللون', - 'Find text': 'بحث عن نص', - 'Move to previous occurrence': 'الانتقال إلى الحدث السابق', - 'Move to next occurrence': 'الانتقال إلى الحدث التالي', - 'Saved using the network': 'تم الحفظ باستخدام الشبكة', - 'Saved using the local storage': - 'تم الحفظ باستخدام وحدة التخزين المحلية', - 'Error while saving image': 'حدث خطأ أثناء حفظ الصورة', - 'Please enter a text for your link': "e.g., 'Learn more)", - 'Please enter the link url': "e.g., 'https://example.com'", - 'Please enter a valid image url': 'Please enter a valid image url' - }, - 'da': { - 'Paste a link': 'Indsæt link', - 'Ok': 'Ok', - 'Select Color': 'Vælg farve', - 'Gallery': 'Galleri', - 'Link': 'Link', - 'Open': 'Åben', - 'Copy': 'Kopi', - 'Remove': 'Fjerne', - 'Save': 'Gemme', - 'Zoom': 'Zoom ind', - 'Saved': 'Gemt', - 'Text': 'Text', - 'Resize': 'Resize', - 'Width': 'Width', - 'Height': 'Height', - 'Size': 'Size', - 'Small': 'Small', - 'Large': 'Large', - 'Huge': 'Huge', - 'Clear': 'Clear', - 'Font': 'Font', - 'Search': 'Search', - 'Camera': 'Camera', - 'Video': 'Video', - 'Undo': 'Undo', - 'Redo': 'Redo', - 'Font family': 'Font family', - 'Font size': 'Font size', - 'Bold': 'Bold', - 'Subscript': 'Subscript', - 'Superscript': 'Superscript', - 'Italic': 'Italic', - 'Underline': 'Underline', - 'Strike through': 'Strike through', - 'Inline code': 'Inline code', - 'Font color': 'Font color', - 'Background color': 'Background color', - 'Clear format': 'Clear format', - 'Align left': 'Align left', - 'Align center': 'Align center', - 'Align right': 'Align right', - 'Justify win width': 'Justify win width', - 'Text direction': 'Text direction', - 'Header style': 'Header style', - 'Numbered list': 'Numbered list', - 'Bullet list': 'Bullet list', - 'Checked list': 'Checked list', - 'Code block': 'Code block', - 'Quote': 'Quote', - 'Increase indent': 'Increase indent', - 'Decrease indent': 'Decrease indent', - 'Insert URL': 'Insert URL', - 'Visit link': 'Visit link', - 'Enter link': 'Enter link', - 'Enter media': 'Enter media', - 'Edit': 'Edit', - 'Apply': 'Apply', - 'Find text': 'Find text', - 'Move to previous occurrence': 'Move to previous occurrence', - 'Move to next occurrence': 'Move to next occurrence', - 'Saved using the network': 'Saved using the network', - 'Saved using the local storage': 'Saved using the local storage', - 'Error while saving image': 'Error while saving image', - 'Please enter a text for your link': "e.g., 'Learn more)", - 'Please enter the link url': "e.g., 'https://example.com'", - 'Please enter a valid image url': 'Please enter a valid image url' - }, - 'de': { - 'Paste a link': 'Link hinzufügen', - 'Ok': 'OK', - 'Select Color': 'Farbe auswählen', - 'Gallery': 'Galerie', - 'Link': 'Link', - 'Open': 'Öffnen', - 'Copy': 'Kopieren', - 'Remove': 'Entfernen', - 'Save': 'Speichern', - 'Zoom': 'Zoomen', - 'Saved': 'Gespeichert', - 'Text': 'Text', - 'Resize': 'Größe ändern', - 'Width': 'Breite', - 'Height': 'Höhe', - 'Size': 'Größe', - 'Small': 'Klein', - 'Large': 'Groß', - 'Huge': 'Riesig', - 'Clear': 'Löschen', - 'Font': 'Schrift', - 'Search': 'Suchen', - 'Camera': 'Kamera', - 'Video': 'Video', - 'Undo': 'Rückgängig', - 'Redo': 'Wiederherstellen', - 'Font family': 'Schriftart', - 'Font size': 'Schriftgröße', - 'Bold': 'Fett', - 'Subscript': 'Tiefgestellt', - 'Superscript': 'Hochgestellt', - 'Italic': 'Kursiv', - 'Underline': 'Unterstreichen', - 'Strike through': 'Durchstreichen', - 'Inline code': 'Inline-Code', - 'Font color': 'Schriftfarbe', - 'Background color': 'Hintergrundfarbe', - 'Clear format': 'Formatierung löschen', - 'Align left': 'Linksbündig ausrichten', - 'Align center': 'Zentriert ausrichten', - 'Align right': 'Rechtsbündig ausrichten', - 'Justify win width': 'Blocksatz', - 'Text direction': 'Textrichtung', - 'Header style': 'Überschrift-Stil', - 'Numbered list': 'Nummerierte Liste', - 'Bullet list': 'Aufzählungsliste', - 'Checked list': 'Checkliste', - 'Code block': 'Code-Block', - 'Quote': 'Zitat', - 'Increase indent': 'Einzug vergrößern', - 'Decrease indent': 'Einzug verkleinern', - 'Insert URL': 'URL einfügen', - 'Visit link': 'Link öffnen', - 'Enter link': 'Link eingeben', - 'Enter media': 'Medien einfügen', - 'Edit': 'Bearbeiten', - 'Apply': 'Anwenden', - 'Find text': 'Text suchen', - 'Move to previous occurrence': 'Zum vorherigen Auftreten springen', - 'Move to next occurrence': 'Zum nächsten Auftreten springen', - 'Saved using the network': 'Saved using the network', - 'Saved using the local storage': 'Saved using the local storage', - 'Error while saving image': 'Error while saving image', - 'Please enter a text for your link': "e.g., 'Learn more)", - 'Please enter the link url': "e.g., 'https://example.com'", - 'Please enter a valid image url': 'Please enter a valid image url' - }, - 'fr': { - 'Paste a link': 'Coller un lien', - 'Ok': 'Ok', - 'Select Color': 'Choisir une couleur', - 'Gallery': 'Galerie', - 'Link': 'Lien', - 'Open': 'Ouvrir', - 'Copy': 'Copier', - 'Remove': 'Supprimer', - 'Save': 'Sauvegarder', - 'Zoom': 'Zoomer', - 'Saved': 'Enregistrée', - 'Text': 'Texte', - 'Resize': 'Redimensionner', - 'Width': 'Largeur', - 'Height': 'Hauteur', - 'Size': 'Taille', - 'Small': 'Petit', - 'Large': 'Grand', - 'Huge': 'Énorme', - 'Clear': 'Supprimer la mise en forme', - 'Font': 'Police', - 'Search': 'Rechercher', - 'Camera': 'Caméra', - 'Video': 'Vidéo', - 'Undo': 'Annuler', - 'Redo': 'Refaire', - 'Font family': 'Famille de police', - 'Font size': 'Taille de police', - 'Bold': 'Gras', - 'Subscript': 'Indice', - 'Superscript': 'Exposant', - 'Italic': 'Italique', - 'Underline': 'Souligné', - 'Strike through': 'Barré', - 'Inline code': 'Code en ligne', - 'Font color': 'Couleur de police', - 'Background color': 'Couleur de fond', - 'Clear format': 'Effacer la mise en forme', - 'Align left': 'Aligner à gauche', - 'Align center': 'Aligner au centre', - 'Align right': 'Aligner à droite', - 'Justify win width': 'Justifier', - 'Text direction': 'Direction du texte', - 'Header style': "Style d'en-tête", - 'Numbered list': 'Liste numérotée', - 'Bullet list': 'Liste à puces', - 'Checked list': 'Check-list', - 'Code block': 'Bloc de code', - 'Quote': 'Citation', - 'Increase indent': 'Augmenter le retrait', - 'Decrease indent': 'Diminuer le retrait', - 'Insert URL': 'Insérer une URL', - 'Visit link': 'Visiter', - 'Enter link': 'Entrer un lien', - 'Enter media': 'Entrer un média', - 'Edit': 'Modifier', - 'Apply': 'Appliquer', - 'Find text': 'Find text', - 'Move to previous occurrence': 'Move to previous occurrence', - 'Move to next occurrence': 'Move to next occurrence', - 'Saved using the network': 'Saved using the network', - 'Saved using the local storage': 'Saved using the local storage', - 'Error while saving image': 'Error while saving image', - 'Please enter a text for your link': "e.g., 'Learn more)", - 'Please enter the link url': "e.g., 'https://example.com'", - 'Please enter a valid image url': 'Please enter a valid image url' - }, - 'zh_cn': { - 'Paste a link': '粘贴链接', - 'Ok': '好', - 'Select Color': '选择颜色', - 'Gallery': '相簿', - 'Link': '链接', - 'Open': '打开', - 'Copy': '复制', - 'Remove': '移除', - 'Save': '保存', - 'Zoom': '放大', - 'Saved': '已保存', - 'Text': '文字', - 'Resize': '调整大小', - 'Width': '宽度', - 'Height': '高度', - 'Size': '文字大小', - 'Small': '小字号', - 'Large': '大字号', - 'Huge': '超大字号', - 'Clear': '清除', - 'Font': '字体', - 'Search': '搜索', - 'Camera': '拍照', - 'Video': '录像', - 'Undo': '撤销', - 'Redo': '重做', - 'Font family': '字体', - 'Font size': '字号', - 'Bold': '粗体', - 'Subscript': '下标', - 'Superscript': '上标', - 'Italic': '斜体', - 'Underline': '下划线', - 'Strike through': '删除线', - 'Inline code': '内联代码', - 'Font color': '字体颜色', - 'Background color': '背景颜色', - 'Clear format': '清除格式', - 'Align left': '左对齐', - 'Align center': '居中对齐', - 'Align right': '右对齐', - 'Justify win width': '两端对齐', - 'Text direction': '文本方向', - 'Header style': '标题样式', - 'Numbered list': '有序列表', - 'Bullet list': '无序列表', - 'Checked list': '任务列表', - 'Code block': '代码块', - 'Quote': '引言', - 'Increase indent': '增加缩进', - 'Decrease indent': '减少缩进', - 'Insert URL': '插入链接', - 'Visit link': '访问链接', - 'Enter link': '输入链接', - 'Enter media': '输入媒体', - 'Edit': '编辑', - 'Apply': '应用', - 'Find text': '搜索文本', - 'Move to previous occurrence': '上一个匹配项', - 'Move to next occurrence': '下一个匹配项', - 'Saved using the network': 'Saved using the network', - 'Saved using the local storage': 'Saved using the local storage', - 'Error while saving image': 'Error while saving image', - 'Please enter a text for your link': "e.g., 'Learn more)", - 'Please enter the link url': "e.g., 'https://example.com'", - 'Please enter a valid image url': 'Please enter a valid image url' - }, - 'zh_hk': { - 'Paste a link': '貼上連結', - 'Ok': '確定', - 'Select Color': '選擇顏色', - 'Gallery': '圖片庫', - 'Link': '連結', - 'Open': '開啓', - 'Copy': '複製', - 'Remove': '移除', - 'Save': '儲存', - 'Zoom': '放大', - 'Saved': '已儲存', - 'Text': '文字', - 'Resize': '變更大小', - 'Width': '寛', - 'Height': '高', - 'Size': '大小', - 'Small': '小', - 'Large': '大', - 'Huge': '超大', - 'Clear': '清除', - 'Font': '字型', - 'Search': '搜尋', - 'Camera': '相機', - 'Video': '錄影', - 'Undo': '撤銷', - 'Redo': '重做', - 'Font family': '字體', - 'Font size': '字號', - 'Bold': '粗體', - 'Subscript': '下標', - 'Superscript': '上標', - 'Italic': '斜體', - 'Underline': '下劃線', - 'Strike through': '刪除線', - 'Inline code': '內聯代碼', - 'Font color': '字體顏色', - 'Background color': '背景顏色', - 'Clear format': '清除格式', - 'Align left': '左對齊', - 'Align center': '居中對齊', - 'Align right': '右對齊', - 'Justify win width': '兩端對齊', - 'Text direction': '文本方向', - 'Header style': '標題樣式', - 'Numbered list': '有序列表', - 'Bullet list': '無序列表', - 'Checked list': '任務列表', - 'Code block': '代碼塊', - 'Quote': '引言', - 'Increase indent': '增加縮進', - 'Decrease indent': '減少縮進', - 'Insert URL': '插入鏈接', - 'Visit link': '訪問鏈接', - 'Enter link': '輸入鏈接', - 'Enter media': '輸入媒體', - 'Edit': '編輯', - 'Apply': '應用', - 'Find text': '搜尋文本', - 'Move to previous occurrence': '上一個匹配項', - 'Move to next occurrence': '下一個匹配項', - 'Saved using the network': 'Saved using the network', - 'Saved using the local storage': 'Saved using the local storage', - 'Error while saving image': 'Error while saving image', - 'Please enter a text for your link': "e.g., 'Learn more)", - 'Please enter the link url': "e.g., 'https://example.com'", - 'Please enter a valid image url': 'Please enter a valid image url' - }, - 'ja': { - 'Paste a link': 'リンクをペースト', - 'Ok': '完了', - 'Select Color': '色を選択', - 'Gallery': '写真集', - 'Link': 'リンク', - 'Open': '開く', - 'Copy': 'コピー', - 'Remove': '削除', - 'Save': '保存', - 'Zoom': '拡大', - 'Saved': '保存済み', - 'Text': '文字', - 'Resize': 'サイズを調整', - 'Width': '幅', - 'Height': '高さ', - 'Size': 'サイズ', - 'Small': '小さい', - 'Large': '大きい', - 'Huge': 'でっかい', - 'Clear': 'クリア', - 'Font': 'フォント', - 'Search': '検索', - 'Camera': 'カメラ', - 'Video': 'ビデオ', - 'Undo': '取り消し', - 'Redo': 'やり直し', - 'Font family': 'フォントファミリー', - 'Font size': 'フォントサイズ', - 'Bold': '太字', - 'Subscript': '下付き', - 'Superscript': '上付き', - 'Italic': '斜体', - 'Underline': '下線', - 'Strike through': '取り消し線', - 'Inline code': 'インラインコード', - 'Font color': 'フォントカラー', - 'Background color': 'ベースカラー', - 'Clear format': 'クリアフォーマット', - 'Align left': '左揃え', - 'Align center': 'センターアライメント', - 'Align right': '右揃え', - 'Justify win width': '両端揃え', - 'Text direction': '文字方向', - 'Header style': 'タイトルスタイル', - 'Numbered list': '順序付きリスト', - 'Bullet list': '順序無しリスト', - 'Checked list': 'チェックボックス', - 'Code block': 'コード', - 'Quote': '引用', - 'Increase indent': 'インデントを増やす', - 'Decrease indent': 'インデントを減らす', - 'Insert URL': 'ハイパーリンクを挿入', - 'Visit link': 'ハイパーリンクを訪問', - 'Enter link': 'ハイパーリンクを輸入', - 'Enter media': 'ミディアムを輸入', - 'Edit': '編集', - 'Apply': '応用', - 'Find text': '検索テキスト', - 'Move to previous occurrence': '前のマッチ', - 'Move to next occurrence': '次のマッチ', - 'Saved using the network': 'Saved using the network', - 'Saved using the local storage': 'Saved using the local storage', - 'Error while saving image': 'Error while saving image', - 'Please enter a text for your link': "e.g., 'Learn more)", - 'Please enter the link url': "e.g., 'https://example.com'", - 'Please enter a valid image url': 'Please enter a valid image url' - }, - 'ko': { - 'Paste a link': '링크를 붙여넣어 주세요.', - 'Ok': '확인', - 'Select Color': '색상 선택', - 'Gallery': '갤러리', - 'Link': '링크', - 'Open': '열기', - 'Copy': '복사하기', - 'Remove': '제거하기', - 'Save': '저장하기', - 'Zoom': '확대하기', - 'Saved': '저장되었습니다.', - 'Text': '텍스트', - 'Resize': '크기조정', - 'Width': '넓이', - 'Height': '높이', - 'Size': '크기', - 'Small': '작게', - 'Large': '크게', - 'Huge': '매우크게', - 'Clear': '초기화', - 'Font': '글꼴', - 'Search': '검색', - 'Camera': '카메라', - 'Video': '비디오', - 'Undo': 'Undo', - 'Redo': 'Redo', - 'Font family': 'Font family', - 'Font size': 'Font size', - 'Bold': 'Bold', - 'Subscript': 'Subscript', - 'Superscript': 'Superscript', - 'Italic': 'Italic', - 'Underline': 'Underline', - 'Strike through': 'Strike through', - 'Inline code': 'Inline code', - 'Font color': 'Font color', - 'Background color': 'Background color', - 'Clear format': 'Clear format', - 'Align left': 'Align left', - 'Align center': 'Align center', - 'Align right': 'Align right', - 'Justify win width': 'Justify win width', - 'Text direction': 'Text direction', - 'Header style': 'Header style', - 'Numbered list': 'Numbered list', - 'Bullet list': 'Bullet list', - 'Checked list': 'Checked list', - 'Code block': 'Code block', - 'Quote': 'Quote', - 'Increase indent': 'Increase indent', - 'Decrease indent': 'Decrease indent', - 'Insert URL': 'Insert URL', - 'Visit link': 'Visit link', - 'Enter link': 'Enter link', - 'Enter media': 'Enter media', - 'Edit': 'Edit', - 'Apply': 'Apply', - 'Find text': 'Find text', - 'Move to previous occurrence': 'Move to previous occurrence', - 'Move to next occurrence': 'Move to next occurrence', - 'Saved using the network': 'Saved using the network', - 'Saved using the local storage': 'Saved using the local storage', - 'Error while saving image': 'Error while saving image', - 'Please enter a text for your link': "e.g., 'Learn more)", - 'Please enter the link url': "e.g., 'https://example.com'", - 'Please enter a valid image url': 'Please enter a valid image url' - }, - 'ru': { - 'Paste a link': 'Вставить ссылку', - 'Ok': 'ОК', - 'Select Color': 'Выбрать цвет', - 'Gallery': 'Галерея', - 'Link': 'Ссылка', - 'Open': 'Открыть', - 'Copy': 'Копировать', - 'Remove': 'Удалить', - 'Save': 'Сохранить', - 'Zoom': 'Увеличить', - 'Saved': 'Сохранено', - 'Text': 'Текст', - 'Resize': 'Resize', - 'Width': 'Width', - 'Height': 'Height', - 'Size': 'Size', - 'Small': 'Small', - 'Large': 'Large', - 'Huge': 'Huge', - 'Clear': 'Clear', - 'Font': 'Font', - 'Search': 'Search', - 'Camera': 'Camera', - 'Video': 'Video', - 'Undo': 'Undo', - 'Redo': 'Redo', - 'Font family': 'Font family', - 'Font size': 'Font size', - 'Bold': 'Bold', - 'Subscript': 'Subscript', - 'Superscript': 'Superscript', - 'Italic': 'Italic', - 'Underline': 'Underline', - 'Strike through': 'Strike through', - 'Inline code': 'Inline code', - 'Font color': 'Font color', - 'Background color': 'Background color', - 'Clear format': 'Clear format', - 'Align left': 'Align left', - 'Align center': 'Align center', - 'Align right': 'Align right', - 'Justify win width': 'Justify win width', - 'Text direction': 'Text direction', - 'Header style': 'Header style', - 'Numbered list': 'Numbered list', - 'Bullet list': 'Bullet list', - 'Checked list': 'Checked list', - 'Code block': 'Code block', - 'Quote': 'Quote', - 'Increase indent': 'Increase indent', - 'Decrease indent': 'Decrease indent', - 'Insert URL': 'Insert URL', - 'Visit link': 'Visit link', - 'Enter link': 'Enter link', - 'Enter media': 'Enter media', - 'Edit': 'Edit', - 'Apply': 'Apply', - 'Find text': 'Find text', - 'Move to previous occurrence': 'Move to previous occurrence', - 'Move to next occurrence': 'Move to next occurrence', - 'Saved using the network': 'Saved using the network', - 'Saved using the local storage': 'Saved using the local storage', - 'Error while saving image': 'Error while saving image', - 'Please enter a text for your link': "e.g., 'Learn more)", - 'Please enter the link url': "e.g., 'https://example.com'", - 'Please enter a valid image url': 'Please enter a valid image url' - }, - 'es': { - 'Paste a link': 'Pega un enlace', - 'Ok': 'Ok', - 'Select Color': 'Selecciona un color', - 'Gallery': 'Galería', - 'Link': 'Enlace', - 'Open': 'Abrir', - 'Copy': 'Copiar', - 'Remove': 'Eliminar', - 'Save': 'Guardar', - 'Zoom': 'Zoom', - 'Saved': 'Guardado', - 'Text': 'Texto', - 'Resize': 'Redimensionar', - 'Width': 'Ancho', - 'Height': 'Alto', - 'Size': 'Tamaño', - 'Small': 'Pequeño', - 'Large': 'Grande', - 'Huge': 'Muy grande', - 'Clear': 'Borrar', - 'Font': 'Fuente', - 'Search': 'Buscar', - 'Camera': 'Cámara', - 'Video': 'Vídeo', - 'Undo': 'Undo', - 'Redo': 'Redo', - 'Font family': 'Font family', - 'Font size': 'Font size', - 'Bold': 'Bold', - 'Subscript': 'Subscript', - 'Superscript': 'Superscript', - 'Italic': 'Italic', - 'Underline': 'Underline', - 'Strike through': 'Strike through', - 'Inline code': 'Inline code', - 'Font color': 'Font color', - 'Background color': 'Background color', - 'Clear format': 'Clear format', - 'Align left': 'Align left', - 'Align center': 'Align center', - 'Align right': 'Align right', - 'Justify win width': 'Justify win width', - 'Text direction': 'Text direction', - 'Header style': 'Header style', - 'Numbered list': 'Numbered list', - 'Bullet list': 'Bullet list', - 'Checked list': 'Checked list', - 'Code block': 'Code block', - 'Quote': 'Quote', - 'Increase indent': 'Increase indent', - 'Decrease indent': 'Decrease indent', - 'Insert URL': 'Insert URL', - 'Visit link': 'Visit link', - 'Enter link': 'Enter link', - 'Enter media': 'Enter media', - 'Edit': 'Edit', - 'Apply': 'Apply', - 'Find text': 'Find text', - 'Move to previous occurrence': 'Move to previous occurrence', - 'Move to next occurrence': 'Move to next occurrence', - 'Saved using the network': 'Saved using the network', - 'Saved using the local storage': 'Saved using the local storage', - 'Error while saving image': 'Error while saving image', - 'Please enter a text for your link': "e.g., 'Learn more)", - 'Please enter the link url': "e.g., 'https://example.com'", - 'Please enter a valid image url': 'Please enter a valid image url' - }, - 'tr': { - 'Paste a link': 'Bağlantıyı Yapıştır', - 'Ok': 'Tamam', - 'Select Color': 'Renk Seçin', - 'Gallery': 'Galeri', - 'Link': 'Bağlantı', - 'Open': 'Açık', - 'Copy': 'Kopyala', - 'Remove': 'Kaldır', - 'Save': 'Kayıt Et', - 'Zoom': 'Yakınlaştır', - 'Saved': 'Kaydedildi', - 'Text': 'Text', - 'Resize': 'Yeniden Boyutlandır', - 'Width': 'Genişlik', - 'Height': 'Yükseklik', - 'Size': 'Boyut', - 'Small': 'Küçük', - 'Large': 'Büyük', - 'Huge': 'Daha Büyük', - 'Clear': 'Temizle', - 'Font': 'Yazı tipi', - 'Search': 'Ara', - 'Camera': 'Kamera', - 'Video': 'Video', - 'Undo': 'Geri', - 'Redo': 'İleri', - 'Font family': 'Yazı Türü', - 'Font size': 'Yazı Boyutu', - 'Bold': 'Kalın', - 'Subscript': 'Alt Simge', - 'Superscript': 'Üst Simge', - 'Italic': 'İtalik', - 'Underline': 'Altı Çizili', - 'Strike through': 'Üsti Çizili', - 'Inline code': 'Inline code', - 'Font color': 'Yazı Rengi', - 'Background color': 'Vurgu Rengi', - 'Clear format': 'Formatı Temizle', - 'Align left': 'Sola Hizala', - 'Align center': 'Ortaya Hizala', - 'Align right': 'Sağa Hizala', - 'Justify win width': 'Kenarlara Hizala', - 'Text direction': 'Metin Yönü', - 'Header style': 'Başlık Stili', - 'Numbered list': 'Numaralı Liste', - 'Bullet list': 'Madde Listesi', - 'Checked list': 'Kontrol Listesi', - 'Code block': 'Kod Blogu', - 'Quote': 'Alıntı', - 'Increase indent': 'Girintiyi Artır', - 'Decrease indent': 'Girintiyi Azalt ', - 'Insert URL': 'URL Giriniz', - 'Visit link': 'Bağlantıyı Ziyaret Et', - 'Enter link': 'Bağlantı Giriniz', - 'Enter media': 'Medya Giriniz', - 'Edit': 'Düzenle', - 'Apply': 'Uygula', - 'Find text': 'Find text', - 'Move to previous occurrence': 'Move to previous occurrence', - 'Move to next occurrence': 'Move to next occurrence', - 'Saved using the network': 'Saved using the network', - 'Saved using the local storage': 'Saved using the local storage', - 'Error while saving image': 'Error while saving image', - 'Please enter a text for your link': "e.g., 'Learn more)", - 'Please enter the link url': "e.g., 'https://example.com'", - 'Please enter a valid image url': 'Please enter a valid image url' - }, - 'uk': { - 'Paste a link': 'Вставити посилання', - 'Ok': 'ОК', - 'Select Color': 'Вибрати колір', - 'Gallery': 'Галерея', - 'Link': 'Посилання', - 'Open': 'Відкрити', - 'Copy': 'Копіювати', - 'Remove': 'Видалити', - 'Save': 'Зберегти', - 'Zoom': 'Збільшити', - 'Saved': 'Збережено', - 'Text': 'Текст', - 'Resize': 'Resize', - 'Width': 'Width', - 'Height': 'Height', - 'Size': 'Size', - 'Small': 'Small', - 'Large': 'Large', - 'Huge': 'Huge', - 'Clear': 'Clear', - 'Font': 'Font', - 'Search': 'Search', - 'Camera': 'Camera', - 'Video': 'Video', - 'Undo': 'Undo', - 'Redo': 'Redo', - 'Font family': 'Font family', - 'Font size': 'Font size', - 'Bold': 'Bold', - 'Subscript': 'Subscript', - 'Superscript': 'Superscript', - 'Italic': 'Italic', - 'Underline': 'Underline', - 'Strike through': 'Strike through', - 'Inline code': 'Inline code', - 'Font color': 'Font color', - 'Background color': 'Background color', - 'Clear format': 'Clear format', - 'Align left': 'Align left', - 'Align center': 'Align center', - 'Align right': 'Align right', - 'Justify win width': 'Justify win width', - 'Text direction': 'Text direction', - 'Header style': 'Header style', - 'Numbered list': 'Numbered list', - 'Bullet list': 'Bullet list', - 'Checked list': 'Checked list', - 'Code block': 'Code block', - 'Quote': 'Quote', - 'Increase indent': 'Increase indent', - 'Decrease indent': 'Decrease indent', - 'Insert URL': 'Insert URL', - 'Visit link': 'Visit link', - 'Enter link': 'Enter link', - 'Enter media': 'Enter media', - 'Edit': 'Edit', - 'Apply': 'Apply', - 'Find text': 'Find text', - 'Move to previous occurrence': 'Move to previous occurrence', - 'Move to next occurrence': 'Move to next occurrence', - 'Saved using the network': 'Saved using the network', - 'Saved using the local storage': 'Saved using the local storage', - 'Error while saving image': 'Error while saving image', - 'Please enter a text for your link': "e.g., 'Learn more)", - 'Please enter the link url': "e.g., 'https://example.com'", - 'Please enter a valid image url': 'Please enter a valid image url' - }, - 'pt': { - 'Paste a link': 'Colar um link', - 'Ok': 'Ok', - 'Select Color': 'Selecionar uma cor', - 'Gallery': 'Galeria', - 'Link': 'Link', - 'Open': 'Abra', - 'Copy': 'Copiar', - 'Remove': 'Remover', - 'Save': 'Salvar', - 'Zoom': 'Zoom', - 'Saved': 'Salvo', - 'Text': 'Texto', - 'Resize': 'Redimencionar', - 'Width': 'Largura', - 'Height': 'Altura', - 'Size': 'Tamanho', - 'Small': 'Pequeno', - 'Large': 'Grande', - 'Huge': 'Gigante', - 'Clear': 'Limpar', - 'Font': 'Fonte', - 'Search': 'Search', - 'Camera': 'Camera', - 'Video': 'Video', - 'Undo': 'Undo', - 'Redo': 'Redo', - 'Font family': 'Font family', - 'Font size': 'Font size', - 'Bold': 'Bold', - 'Subscript': 'Subscript', - 'Superscript': 'Superscript', - 'Italic': 'Italic', - 'Underline': 'Underline', - 'Strike through': 'Strike through', - 'Inline code': 'Inline code', - 'Font color': 'Font color', - 'Background color': 'Background color', - 'Clear format': 'Clear format', - 'Align left': 'Align left', - 'Align center': 'Align center', - 'Align right': 'Align right', - 'Justify win width': 'Justify win width', - 'Text direction': 'Text direction', - 'Header style': 'Header style', - 'Numbered list': 'Numbered list', - 'Bullet list': 'Bullet list', - 'Checked list': 'Checked list', - 'Code block': 'Code block', - 'Quote': 'Quote', - 'Increase indent': 'Increase indent', - 'Decrease indent': 'Decrease indent', - 'Insert URL': 'Insert URL', - 'Visit link': 'Visit link', - 'Enter link': 'Enter link', - 'Enter media': 'Enter media', - 'Edit': 'Edit', - 'Apply': 'Apply', - 'Find text': 'Find text', - 'Move to previous occurrence': 'Move to previous occurrence', - 'Move to next occurrence': 'Move to next occurrence', - 'Saved using the network': 'Guardado através da network', - 'Saved using the local storage': - 'Guardado através do armazenamento local', - 'Error while saving image': 'Erro a gravar imagem', - 'Please enter a text for your link': "e.g., 'Learn more)", - 'Please enter the link url': "e.g., 'https://example.com'", - 'Please enter a valid image url': 'Please enter a valid image url' - }, - 'pt_br': { - 'Paste a link': 'Colar um link', - 'Ok': 'Ok', - 'Select Color': 'Selecionar uma cor', - 'Gallery': 'Galeria', - 'Link': 'Link', - 'Open': 'Abrir', - 'Copy': 'Copiar', - 'Remove': 'Remover', - 'Save': 'Salvar', - 'Zoom': 'Zoom', - 'Saved': 'Salvo', - 'Text': 'Texto', - 'Resize': 'Redimensionar', - 'Width': 'Largura', - 'Height': 'Altura', - 'Size': 'Tamanho', - 'Small': 'Pequeno', - 'Large': 'Grande', - 'Huge': 'Gigante', - 'Clear': 'Limpar', - 'Font': 'Fonte', - 'Search': 'Buscar', - 'Camera': 'Câmera', - 'Video': 'Vídeo', - 'Undo': 'Desfazer', - 'Redo': 'Refazer', - 'Font family': 'Fonte', - 'Font size': 'Tamanho da fonte', - 'Bold': 'Negrito', - 'Subscript': 'Subscrito', - 'Superscript': 'Sobrescrito', - 'Italic': 'Itálico', - 'Underline': 'Sublinhado', - 'Strike through': 'Tachado', - 'Inline code': 'Inline code', - 'Font color': 'Cor da fonte', - 'Background color': 'Cor do fundo', - 'Clear format': 'Limpar formatação', - 'Align left': 'Texto à esquerda', - 'Align center': 'Centralizar', - 'Align right': 'Texto à direita', - 'Justify win width': 'Justificado', - 'Text direction': 'Direção do texto', - 'Header style': 'Estilo de cabeçalho', - 'Numbered list': 'Numeração', - 'Bullet list': 'Marcadores', - 'Checked list': 'Lista de verificação', - 'Code block': 'Code block', - 'Quote': 'Citação', - 'Increase indent': 'Aumentar recuo', - 'Decrease indent': 'Diminuir recuo', - 'Insert URL': 'Inserir URL', - 'Visit link': 'Visitar link', - 'Enter link': 'Inserir link', - 'Enter media': 'Inserir mídia', - 'Edit': 'Editar', - 'Apply': 'Aplicar', - 'Find text': 'Find text', - 'Move to previous occurrence': 'Move to previous occurrence', - 'Move to next occurrence': 'Move to next occurrence', - 'Saved using the network': 'Saved using the network', - 'Saved using the local storage': 'Saved using the local storage', - 'Error while saving image': 'Error while saving image', - 'Please enter a text for your link': "e.g., 'Learn more)", - 'Please enter the link url': "e.g., 'https://example.com'", - 'Please enter a valid image url': 'Please enter a valid image url' - }, - 'pl': { - 'Paste a link': 'Wklej link', - 'Ok': 'OK', - 'Select Color': 'Wybierz kolor', - 'Gallery': 'Galeria', - 'Link': 'Link', - 'Open': 'Otwórz', - 'Copy': 'Kopiuj', - 'Remove': 'Usuń', - 'Save': 'Zapisz', - 'Zoom': 'Powiększenie', - 'Saved': 'Zapisano', - 'Text': 'Tekst', - 'Resize': 'Resize', - 'Width': 'Width', - 'Height': 'Height', - 'Size': 'Size', - 'Small': 'Small', - 'Large': 'Large', - 'Huge': 'Huge', - 'Clear': 'Clear', - 'Font': 'Font', - 'Search': 'Search', - 'Camera': 'Camera', - 'Video': 'Video', - 'Undo': 'Undo', - 'Redo': 'Redo', - 'Font family': 'Font family', - 'Font size': 'Font size', - 'Bold': 'Bold', - 'Subscript': 'Subscript', - 'Superscript': 'Superscript', - 'Italic': 'Italic', - 'Underline': 'Underline', - 'Strike through': 'Strike through', - 'Inline code': 'Inline code', - 'Font color': 'Font color', - 'Background color': 'Background color', - 'Clear format': 'Clear format', - 'Align left': 'Align left', - 'Align center': 'Align center', - 'Align right': 'Align right', - 'Justify win width': 'Justify win width', - 'Text direction': 'Text direction', - 'Header style': 'Header style', - 'Numbered list': 'Numbered list', - 'Bullet list': 'Bullet list', - 'Checked list': 'Checked list', - 'Code block': 'Code block', - 'Quote': 'Quote', - 'Increase indent': 'Increase indent', - 'Decrease indent': 'Decrease indent', - 'Insert URL': 'Insert URL', - 'Visit link': 'Visit link', - 'Enter link': 'Enter link', - 'Enter media': 'Enter media', - 'Edit': 'Edit', - 'Apply': 'Apply', - 'Find text': 'Find text', - 'Move to previous occurrence': 'Move to previous occurrence', - 'Move to next occurrence': 'Move to next occurrence', - 'Saved using the network': 'Saved using the network', - 'Saved using the local storage': 'Saved using the local storage', - 'Error while saving image': 'Error while saving image', - 'Please enter a text for your link': "e.g., 'Learn more)", - 'Please enter the link url': "e.g., 'https://example.com'", - 'Please enter a valid image url': 'Please enter a valid image url' - }, - 'vi': { - 'Paste a link': 'Chèn liên kết', - 'Ok': 'OK', - 'Select Color': 'Chọn Màu', - 'Gallery': 'Thư viện', - 'Link': 'Liên kết', - 'Open': 'Mở', - 'Copy': 'Sao chép', - 'Remove': 'Xoá', - 'Save': 'Lưu', - 'Zoom': 'Thu phóng', - 'Saved': 'Đã lưu', - 'Text': 'Chữ', - 'Resize': 'Resize', - 'Width': 'Rộng', - 'Height': 'Cao', - 'Size': 'Kích thước', - 'Small': 'Nhỏ', - 'Large': 'Lớn', - 'Huge': 'Rất lớn', - 'Clear': 'Xoá', - 'Font': 'Phông chữ', - 'Search': 'Tìm', - 'Camera': 'Máy ảnh', - 'Video': 'Video', - 'Undo': 'Undo', - 'Redo': 'Redo', - 'Font family': 'Phông chữ', - 'Font size': 'Cỡ chữ', - 'Bold': 'Đậm', - 'Subscript': 'Chèn dưới', - 'Superscript': 'Chèn trên', - 'Italic': 'Nghiêng', - 'Underline': 'Gạch chân', - 'Strike through': 'Gạch ngang', - 'Inline code': 'Dòng mã', - 'Font color': 'Màu chữ', - 'Background color': 'Màu nền', - 'Clear format': 'Xoá định dạng', - 'Align left': 'Căn trái', - 'Align center': 'Căn giữa', - 'Align right': 'Căn phải', - 'Justify win width': 'Căn đều chiều rộng', - 'Text direction': 'Text direction', - 'Header style': 'Header style', - 'Numbered list': 'Danh sách thứ tự', - 'Bullet list': 'Danh sách', - 'Checked list': 'Danh sách hộp kiểm', - 'Code block': 'Đoạn mã', - 'Quote': 'Quote', - 'Increase indent': 'Increase indent', - 'Decrease indent': 'Decrease indent', - 'Insert URL': 'Thêm liên kết', - 'Visit link': 'Mở liên kết', - 'Enter link': 'Nhập liên kết', - 'Enter media': 'Enter media', - 'Edit': 'Sửa', - 'Apply': 'Áp dụng', - 'Find text': 'Find text', - 'Move to previous occurrence': 'Move to previous occurrence', - 'Move to next occurrence': 'Move to next occurrence', - 'Saved using the network': 'Saved using the network', - 'Saved using the local storage': 'Saved using the local storage', - 'Error while saving image': 'Error while saving image', - 'Please enter a text for your link': "e.g., 'Learn more)", - 'Please enter the link url': "e.g., 'https://example.com'", - 'Please enter a valid image url': 'Please enter a valid image url' - }, - 'ur': { - 'Paste a link': 'لنک پیسٹ کریں', - 'Ok': 'ٹھیک ہے', - 'Select Color': 'رنگ منتخب کریں', - 'Gallery': 'گیلری', - 'Link': 'لنک', - 'Open': 'کھولیں', - 'Copy': 'نقل', - 'Remove': 'ہٹا دیں', - 'Save': 'محفوظ کریں', - 'Zoom': 'زوم', - 'Saved': 'محفوظ کر لیا', - 'Text': 'متن', - 'Resize': 'سائز تبدیل کریں۔', - 'Width': 'چوڑائی', - 'Height': 'اونچائی', - 'Size': 'Size', - 'Small': 'Small', - 'Large': 'Large', - 'Huge': 'Huge', - 'Clear': 'Clear', - 'Font': 'Font', - 'Search': 'Search', - 'Camera': 'Camera', - 'Video': 'Video', - 'Undo': 'Undo', - 'Redo': 'Redo', - 'Font family': 'Font family', - 'Font size': 'Font size', - 'Bold': 'Bold', - 'Subscript': 'Subscript', - 'Superscript': 'Superscript', - 'Italic': 'Italic', - 'Underline': 'Underline', - 'Strike through': 'Strike through', - 'Inline code': 'Inline code', - 'Font color': 'Font color', - 'Background color': 'Background color', - 'Clear format': 'Clear format', - 'Align left': 'Align left', - 'Align center': 'Align center', - 'Align right': 'Align right', - 'Justify win width': 'Justify win width', - 'Text direction': 'Text direction', - 'Header style': 'Header style', - 'Numbered list': 'Numbered list', - 'Bullet list': 'Bullet list', - 'Checked list': 'Checked list', - 'Code block': 'Code block', - 'Quote': 'Quote', - 'Increase indent': 'Increase indent', - 'Decrease indent': 'Decrease indent', - 'Insert URL': 'Insert URL', - 'Visit link': 'Visit link', - 'Enter link': 'Enter link', - 'Enter media': 'Enter media', - 'Edit': 'Edit', - 'Apply': 'Apply', - 'Find text': 'Find text', - 'Move to previous occurrence': 'Move to previous occurrence', - 'Move to next occurrence': 'Move to next occurrence', - 'Saved using the network': 'Saved using the network', - 'Saved using the local storage': 'Saved using the local storage', - 'Error while saving image': 'Error while saving image', - 'Please enter a text for your link': "e.g., 'Learn more)", - 'Please enter the link url': "e.g., 'https://example.com'", - 'Please enter a valid image url': 'Please enter a valid image url' - }, - 'id': { - 'Paste a link': 'Tempel tautan', - 'Ok': 'Oke', - 'Select Color': 'Pilih Warna', - 'Gallery': 'Galeri', - 'Link': 'Tautan', - 'Open': 'Buka', - 'Copy': 'Salin', - 'Remove': 'Hapus', - 'Save': 'Simpan', - 'Zoom': 'Perbesar', - 'Saved': 'Tersimpan', - 'Text': 'Teks', - 'Resize': 'Ubah Ukuran', - 'Width': 'Lebar', - 'Height': 'Tinggi', - 'Size': 'Size', - 'Small': 'Small', - 'Large': 'Large', - 'Huge': 'Huge', - 'Clear': 'Clear', - 'Font': 'Font', - 'Search': 'Search', - 'Camera': 'Camera', - 'Video': 'Video', - 'Undo': 'Undo', - 'Redo': 'Redo', - 'Font family': 'Font family', - 'Font size': 'Font size', - 'Bold': 'Bold', - 'Subscript': 'Subscript', - 'Superscript': 'Superscript', - 'Italic': 'Italic', - 'Underline': 'Underline', - 'Strike through': 'Strike through', - 'Inline code': 'Inline code', - 'Font color': 'Font color', - 'Background color': 'Background color', - 'Clear format': 'Clear format', - 'Align left': 'Align left', - 'Align center': 'Align center', - 'Align right': 'Align right', - 'Justify win width': 'Justify win width', - 'Text direction': 'Text direction', - 'Header style': 'Header style', - 'Numbered list': 'Numbered list', - 'Bullet list': 'Bullet list', - 'Checked list': 'Checked list', - 'Code block': 'Code block', - 'Quote': 'Quote', - 'Increase indent': 'Increase indent', - 'Decrease indent': 'Decrease indent', - 'Insert URL': 'Insert URL', - 'Visit link': 'Visit link', - 'Enter link': 'Enter link', - 'Enter media': 'Enter media', - 'Edit': 'Edit', - 'Apply': 'Apply', - 'Find text': 'Find text', - 'Move to previous occurrence': 'Move to previous occurrence', - 'Move to next occurrence': 'Move to next occurrence', - 'Saved using the network': 'Saved using the network', - 'Saved using the local storage': 'Saved using the local storage', - 'Error while saving image': 'Error while saving image', - 'Please enter a text for your link': "e.g., 'Learn more)", - 'Please enter the link url': "e.g., 'https://example.com'", - 'Please enter a valid image url': 'Please enter a valid image url' - }, - 'no': { - 'Paste a link': 'Lim inn lenke', - 'Ok': 'Ok', - 'Select Color': 'Velg farge', - 'Gallery': 'Galleri', - 'Link': 'Lenke', - 'Open': 'Åpne', - 'Copy': 'Kopier', - 'Remove': 'Fjern', - 'Save': 'Lagre', - 'Zoom': 'Zoom', - 'Saved': 'Lagret', - 'Text': 'Tekst', - 'Resize': 'Endre størrelse', - 'Width': 'Bredde', - 'Height': 'Høyde', - 'Size': 'Size', - 'Small': 'Small', - 'Large': 'Large', - 'Huge': 'Huge', - 'Clear': 'Clear', - 'Font': 'Font', - 'Search': 'Search', - 'Camera': 'Camera', - 'Video': 'Video', - 'Undo': 'Undo', - 'Redo': 'Redo', - 'Font family': 'Font family', - 'Font size': 'Font size', - 'Bold': 'Bold', - 'Subscript': 'Subscript', - 'Superscript': 'Superscript', - 'Italic': 'Italic', - 'Underline': 'Underline', - 'Strike through': 'Strike through', - 'Inline code': 'Inline code', - 'Font color': 'Font color', - 'Background color': 'Background color', - 'Clear format': 'Clear format', - 'Align left': 'Align left', - 'Align center': 'Align center', - 'Align right': 'Align right', - 'Justify win width': 'Justify win width', - 'Text direction': 'Text direction', - 'Header style': 'Header style', - 'Numbered list': 'Numbered list', - 'Bullet list': 'Bullet list', - 'Checked list': 'Checked list', - 'Code block': 'Code block', - 'Quote': 'Quote', - 'Increase indent': 'Increase indent', - 'Decrease indent': 'Decrease indent', - 'Insert URL': 'Insert URL', - 'Visit link': 'Visit link', - 'Enter link': 'Enter link', - 'Enter media': 'Enter media', - 'Edit': 'Edit', - 'Apply': 'Apply', - 'Find text': 'Find text', - 'Move to previous occurrence': 'Move to previous occurrence', - 'Move to next occurrence': 'Move to next occurrence', - 'Saved using the network': 'Saved using the network', - 'Saved using the local storage': 'Saved using the local storage', - 'Error while saving image': 'Error while saving image', - 'Please enter a text for your link': "e.g., 'Learn more)", - 'Please enter the link url': "e.g., 'https://example.com'", - 'Please enter a valid image url': 'Please enter a valid image url' - }, - 'fa': { - 'Paste a link': 'جایگذاری لینک', - 'Ok': 'تایید', - 'Select Color': 'انتخاب رنگ', - 'Gallery': 'گالری', - 'Link': 'لینک', - 'Open': 'باز کردن', - 'Copy': 'کپی', - 'Remove': 'حذف', - 'Save': 'ذخیره', - 'Zoom': 'بزرگنمایی', - 'Saved': 'ذخیره شد', - 'Text': 'متن', - 'Resize': 'تغییر اندازه', - 'Width': 'عرض', - 'Height': 'طول', - 'Size': 'اندازه', - 'Small': 'کوچک', - 'Large': 'بزرگ', - 'Huge': 'خیلی بزرگ', - 'Clear': 'پاک کردن', - 'Font': 'فونت', - 'Search': 'جستجو', - 'Camera': 'دوربین', - 'Video': 'ویدیو', - 'Undo': 'Undo', - 'Redo': 'Redo', - 'Font family': 'Font family', - 'Font size': 'Font size', - 'Bold': 'Bold', - 'Subscript': 'Subscript', - 'Superscript': 'Superscript', - 'Italic': 'Italic', - 'Underline': 'Underline', - 'Strike through': 'Strike through', - 'Inline code': 'Inline code', - 'Font color': 'Font color', - 'Background color': 'Background color', - 'Clear format': 'Clear format', - 'Align left': 'Align left', - 'Align center': 'Align center', - 'Align right': 'Align right', - 'Justify win width': 'Justify win width', - 'Text direction': 'Text direction', - 'Header style': 'Header style', - 'Numbered list': 'Numbered list', - 'Bullet list': 'Bullet list', - 'Checked list': 'Checked list', - 'Code block': 'Code block', - 'Quote': 'Quote', - 'Increase indent': 'Increase indent', - 'Decrease indent': 'Decrease indent', - 'Insert URL': 'Insert URL', - 'Visit link': 'Visit link', - 'Enter link': 'Enter link', - 'Enter media': 'Enter media', - 'Edit': 'Edit', - 'Apply': 'Apply', - 'Find text': 'Find text', - 'Move to previous occurrence': 'Move to previous occurrence', - 'Move to next occurrence': 'Move to next occurrence', - 'Saved using the network': 'Saved using the network', - 'Saved using the local storage': 'Saved using the local storage', - 'Error while saving image': 'Error while saving image', - 'Please enter a text for your link': "e.g., 'Learn more)", - 'Please enter the link url': "e.g., 'https://example.com'", - 'Please enter a valid image url': 'Please enter a valid image url' - }, - 'hi': { - 'Paste a link': 'लिंक पेस्ट करें', - 'Ok': 'ठीक है', - 'Select Color': 'रंग चुनें', - 'Gallery': 'गैलरी', - 'Link': 'लिंक', - 'Open': 'खोलें', - 'Copy': 'कॉपी करें', - 'Remove': 'हटाएं', - 'Save': 'सुरक्षित करें', - 'Zoom': 'बड़ा करें', - 'Saved': 'सुरक्षित कर दिया गया है', - 'Text': 'शब्द', - 'Resize': 'आकार बदलें', - 'Width': 'चौड़ाई', - 'Height': 'ऊंचाई', - 'Size': 'Size', - 'Small': 'Small', - 'Large': 'Large', - 'Huge': 'Huge', - 'Clear': 'Clear', - 'Font': 'Font', - 'Search': 'Search', - 'Camera': 'Camera', - 'Video': 'Video', - 'Undo': 'Undo', - 'Redo': 'Redo', - 'Font family': 'Font family', - 'Font size': 'Font size', - 'Bold': 'Bold', - 'Subscript': 'Subscript', - 'Superscript': 'Superscript', - 'Italic': 'Italic', - 'Underline': 'Underline', - 'Strike through': 'Strike through', - 'Inline code': 'Inline code', - 'Font color': 'Font color', - 'Background color': 'Background color', - 'Clear format': 'Clear format', - 'Align left': 'Align left', - 'Align center': 'Align center', - 'Align right': 'Align right', - 'Justify win width': 'Justify win width', - 'Text direction': 'Text direction', - 'Header style': 'Header style', - 'Numbered list': 'Numbered list', - 'Bullet list': 'Bullet list', - 'Checked list': 'Checked list', - 'Code block': 'Code block', - 'Quote': 'Quote', - 'Increase indent': 'Increase indent', - 'Decrease indent': 'Decrease indent', - 'Insert URL': 'Insert URL', - 'Visit link': 'Visit link', - 'Enter link': 'Enter link', - 'Enter media': 'Enter media', - 'Edit': 'Edit', - 'Apply': 'Apply', - 'Find text': 'Find text', - 'Move to previous occurrence': 'Move to previous occurrence', - 'Move to next occurrence': 'Move to next occurrence', - 'Saved using the network': 'Saved using the network', - 'Saved using the local storage': 'Saved using the local storage', - 'Error while saving image': 'Error while saving image', - 'Please enter a text for your link': "e.g., 'Learn more)", - 'Please enter the link url': "e.g., 'https://example.com'", - 'Please enter a valid image url': 'Please enter a valid image url' - }, - 'nl': { - 'Paste a link': 'Plak een link', - 'Ok': 'Ok', - 'Select Color': 'Selecteer kleur', - 'Gallery': 'Gallerij', - 'Link': 'Link', - 'Open': 'Open', - 'Copy': 'Kopieer', - 'Remove': 'Verwijderd', - 'Save': 'Opslaan', - 'Zoom': 'Zoom', - 'Saved': 'Opgeslagen', - 'Text': 'Tekst', - 'Resize': 'Formaat wijzigen', - 'Width': 'Breedte', - 'Height': 'Hoogte', - 'Size': 'Grootte', - 'Small': 'Small', - 'Large': 'Large', - 'Huge': 'Huge', - 'Clear': 'Clear', - 'Font': 'Font', - 'Search': 'Search', - 'Camera': 'Camera', - 'Video': 'Video', - 'Undo': 'Undo', - 'Redo': 'Redo', - 'Font family': 'Font family', - 'Font size': 'Font size', - 'Bold': 'Bold', - 'Subscript': 'Subscript', - 'Superscript': 'Superscript', - 'Italic': 'Italic', - 'Underline': 'Underline', - 'Strike through': 'Strike through', - 'Inline code': 'Inline code', - 'Font color': 'Font color', - 'Background color': 'Background color', - 'Clear format': 'Clear format', - 'Align left': 'Align left', - 'Align center': 'Align center', - 'Align right': 'Align right', - 'Justify win width': 'Justify win width', - 'Text direction': 'Text direction', - 'Header style': 'Header style', - 'Numbered list': 'Numbered list', - 'Bullet list': 'Bullet list', - 'Checked list': 'Checked list', - 'Code block': 'Code block', - 'Quote': 'Quote', - 'Increase indent': 'Increase indent', - 'Decrease indent': 'Decrease indent', - 'Insert URL': 'Insert URL', - 'Visit link': 'Visit link', - 'Enter link': 'Enter link', - 'Enter media': 'Enter media', - 'Edit': 'Edit', - 'Apply': 'Apply', - 'Find text': 'Find text', - 'Move to previous occurrence': 'Move to previous occurrence', - 'Move to next occurrence': 'Move to next occurrence', - 'Saved using the network': 'Saved using the network', - 'Saved using the local storage': 'Saved using the local storage', - 'Error while saving image': 'Error while saving image', - 'Please enter a text for your link': "e.g., 'Learn more)", - 'Please enter the link url': "e.g., 'https://example.com'", - 'Please enter a valid image url': 'Please enter a valid image url' - }, - 'sr': { - 'Paste a link': 'Nalepi vezu', - 'Ok': 'OK', - 'Select Color': 'Odaberi boju', - 'Gallery': 'Galerija', - 'Link': 'Veza', - 'Open': 'Otvori', - 'Copy': 'Kopiraj', - 'Remove': 'Ukloni', - 'Save': 'Sačuvaj', - 'Zoom': 'Uvećaj', - 'Saved': 'Sačuvano', - 'Text': 'Tekst', - 'Resize': 'Promeni veličinu', - 'Width': 'Širina', - 'Height': 'Visina', - 'Size': 'Veličina', - 'Small': 'Small', - 'Large': 'Large', - 'Huge': 'Huge', - 'Clear': 'Clear', - 'Font': 'Font', - 'Search': 'Search', - 'Camera': 'Camera', - 'Video': 'Video', - 'Undo': 'Undo', - 'Redo': 'Redo', - 'Font family': 'Font family', - 'Font size': 'Font size', - 'Bold': 'Bold', - 'Subscript': 'Subscript', - 'Superscript': 'Superscript', - 'Italic': 'Italic', - 'Underline': 'Underline', - 'Strike through': 'Strike through', - 'Inline code': 'Inline code', - 'Font color': 'Font color', - 'Background color': 'Background color', - 'Clear format': 'Clear format', - 'Align left': 'Align left', - 'Align center': 'Align center', - 'Align right': 'Align right', - 'Justify win width': 'Justify win width', - 'Text direction': 'Text direction', - 'Header style': 'Header style', - 'Numbered list': 'Numbered list', - 'Bullet list': 'Bullet list', - 'Checked list': 'Checked list', - 'Code block': 'Code block', - 'Quote': 'Quote', - 'Increase indent': 'Increase indent', - 'Decrease indent': 'Decrease indent', - 'Insert URL': 'Insert URL', - 'Visit link': 'Visit link', - 'Enter link': 'Enter link', - 'Enter media': 'Enter media', - 'Edit': 'Edit', - 'Apply': 'Apply', - 'Find text': 'Find text', - 'Move to previous occurrence': 'Move to previous occurrence', - 'Move to next occurrence': 'Move to next occurrence', - 'Saved using the network': 'Saved using the network', - 'Saved using the local storage': 'Saved using the local storage', - 'Error while saving image': 'Error while saving image', - 'Please enter a text for your link': "e.g., 'Learn more)", - 'Please enter the link url': "e.g., 'https://example.com'", - 'Please enter a valid image url': 'Please enter a valid image url' - }, - 'cs': { - 'Paste a link': 'Vložit odkaz', - 'Ok': 'Ok', - 'Select Color': 'Vybrat barvu', - 'Gallery': 'Galerie', - 'Link': 'Odkaz', - 'Open': 'Otevřít', - 'Copy': 'Kopírovat', - 'Remove': 'Odstranit', - 'Save': 'Uložit', - 'Zoom': 'Přiblížit', - 'Saved': 'Uloženo', - 'Text': 'Text', - 'Resize': 'Změnit velikost', - 'Width': 'Šířka', - 'Height': 'Výška', - 'Size': 'Velikost', - 'Small': 'Malý', - 'Large': 'Velký', - 'Huge': 'Obrovský', - 'Clear': 'Smazat', - 'Font': 'Písmo', - 'Search': 'Hledat', - 'Camera': 'Kamera', - 'Video': 'Video', - 'Undo': 'Zpět', - 'Redo': 'Znovu', - 'Font family': 'Rodina písma', - 'Font size': 'Velikost písma', - 'Bold': 'Tučné', - 'Subscript': 'Dolní index', - 'Superscript': 'Horní index', - 'Italic': 'Kurzíva', - 'Underline': 'Podtržení', - 'Strike through': 'Přeškrtnuté', - 'Inline code': 'Inline kód', - 'Font color': 'Barva písma', - 'Background color': 'Barva pozadí', - 'Clear format': 'Vymazat formátování', - 'Align left': 'Zarovnat vlevo', - 'Align center': 'Zarovnat na střed', - 'Align right': 'Zarovnat vpravo', - 'Justify win width': 'Zarovnat do bloku', - 'Text direction': 'Směr textu', - 'Header style': 'Styl záhlaví', - 'Numbered list': 'Číslovaný seznam', - 'Bullet list': 'Seznam s odrážkami', - 'Checked list': 'Seznam s zaškrtávacími políčky', - 'Code block': 'Blokový kód', - 'Quote': 'Citace', - 'Increase indent': 'Zvětšit odsazení', - 'Decrease indent': 'Zmenšit odsazení', - 'Insert URL': 'Vložit URL', - 'Visit link': 'Otevřít odkaz', - 'Enter link': 'Vložit odkaz', - 'Enter media': 'Vložit média', - 'Edit': 'Upravit', - 'Apply': 'Použít', - 'Hex': 'Hex', - 'Material': 'Material', - 'Color': 'Barva', - 'Find text': 'Najít text', - 'Move to previous occurrence': 'Přesunout na předchozí výskyt', - 'Move to next occurrence': 'Přesunout na následující výskyt', - 'Saved using the network': 'Uloženo pomocí sítě', - 'Saved using local storage': 'Uloženo lokálně', - 'Error while saving image': 'Chyba při ukládání obrázku', - 'Please enter a text for your link': "e.g., 'Learn more)", - 'Please enter the link url': "e.g., 'https://example.com'", - 'Please enter a valid image url': 'Please enter a valid image url' - }, - 'he': { - 'Paste a link': 'הדבק את הלינק', - 'Ok': 'אוקי', - 'Select Color': 'בחר צבע', - 'Gallery': 'גלריה', - 'Link': 'לינק', - 'Open': 'פתח', - 'Copy': 'העתק', - 'Remove': 'מחק', - 'Save': 'שמור', - 'Zoom': 'זום', - 'Saved': 'נשמר', - 'Text': 'טקסט', - 'Resize': 'שנה גודל', - 'Width': 'רוחב', - 'Height': 'גובה', - 'Size': 'גודל', - 'Small': 'קטן', - 'Large': 'גדול', - 'Huge': 'ענק', - 'Clear': 'מחוק', - 'Font': 'פונט', - 'Search': 'חפש', - 'Camera': 'מצלמה', - 'Video': 'וידאו', - 'Undo': 'Undo', - 'Redo': 'Redo', - 'Font family': 'Font family', - 'Font size': 'Font size', - 'Bold': 'Bold', - 'Subscript': 'Subscript', - 'Superscript': 'Superscript', - 'Italic': 'Italic', - 'Underline': 'Underline', - 'Strike through': 'Strike through', - 'Inline code': 'Inline code', - 'Font color': 'Font color', - 'Background color': 'Background color', - 'Clear format': 'Clear format', - 'Align left': 'Align left', - 'Align center': 'Align center', - 'Align right': 'Align right', - 'Justify win width': 'Justify win width', - 'Text direction': 'Text direction', - 'Header style': 'Header style', - 'Numbered list': 'Numbered list', - 'Bullet list': 'Bullet list', - 'Checked list': 'Checked list', - 'Code block': 'Code block', - 'Quote': 'Quote', - 'Increase indent': 'Increase indent', - 'Decrease indent': 'Decrease indent', - 'Insert URL': 'Insert URL', - 'Visit link': 'Visit link', - 'Enter link': 'Enter link', - 'Enter media': 'Enter media', - 'Edit': 'Edit', - 'Apply': 'Apply', - 'Find text': 'Find text', - 'Move to previous occurrence': 'Move to previous occurrence', - 'Move to next occurrence': 'Move to next occurrence', - 'Saved using the network': 'Saved using the network', - 'Saved using the local storage': 'Saved using the local storage', - 'Error while saving image': 'Error while saving image', - 'Please enter a text for your link': "e.g., 'Learn more)", - 'Please enter the link url': "e.g., 'https://example.com'", - 'Please enter a valid image url': 'Please enter a valid image url' - }, - 'ms': { - 'Paste a link': 'Tampal Pautan', - 'Ok': 'Ok', - 'Select Color': 'Pilih Warna', - 'Gallery': 'Galeri', - 'Link': 'Pautan', - 'Open': 'Buka', - 'Copy': 'Salin', - 'Remove': 'Buang', - 'Save': 'Simpan', - 'Zoom': 'Zum', - 'Saved': 'Telah Disimpan', - 'Text': 'Perkataan', - 'Resize': 'Ubah saiz', - 'Width': 'Lebar', - 'Height': 'Tinggi', - 'Size': 'Saiz', - 'Small': 'Kecil', - 'Large': 'Besar', - 'Huge': 'Amat Besar', - 'Clear': 'Padam', - 'Font': 'Fon', - 'Search': 'Carian', - 'Camera': 'Kamera', - 'Video': 'Video', - 'Undo': 'Undo', - 'Redo': 'Redo', - 'Font family': 'Font family', - 'Font size': 'Font size', - 'Bold': 'Bold', - 'Subscript': 'Subscript', - 'Superscript': 'Superscript', - 'Italic': 'Italic', - 'Underline': 'Underline', - 'Strike through': 'Strike through', - 'Inline code': 'Inline code', - 'Font color': 'Font color', - 'Background color': 'Background color', - 'Clear format': 'Clear format', - 'Align left': 'Align left', - 'Align center': 'Align center', - 'Align right': 'Align right', - 'Justify win width': 'Justify win width', - 'Text direction': 'Text direction', - 'Header style': 'Header style', - 'Numbered list': 'Numbered list', - 'Bullet list': 'Bullet list', - 'Checked list': 'Checked list', - 'Code block': 'Code block', - 'Quote': 'Quote', - 'Increase indent': 'Increase indent', - 'Decrease indent': 'Decrease indent', - 'Insert URL': 'Insert URL', - 'Visit link': 'Visit link', - 'Enter link': 'Enter link', - 'Enter media': 'Enter media', - 'Edit': 'Edit', - 'Apply': 'Apply', - 'Find text': 'Find text', - 'Move to previous occurrence': 'Move to previous occurrence', - 'Move to next occurrence': 'Move to next occurrence', - 'Saved using the network': 'Saved using the network', - 'Saved using the local storage': 'Saved using the local storage', - 'Error while saving image': 'Error while saving image', - 'Please enter a text for your link': "e.g., 'Learn more)", - 'Please enter the link url': "e.g., 'https://example.com'", - 'Please enter a valid image url': 'Please enter a valid image url' - }, - 'it': { - 'Paste a link': 'Incolla un collegamento', - 'Ok': 'Ok', - 'Select Color': 'Seleziona Colore', - 'Gallery': 'Galleria', - 'Link': 'Collegamento', - 'Open': 'Apri', - 'Copy': 'Copia', - 'Remove': 'Rimuovi', - 'Save': 'Salva', - 'Zoom': 'Ingrandisci', - 'Saved': 'Salvato', - 'Text': 'Testo', - 'Resize': 'Ridimensiona', - 'Width': 'Larghezza', - 'Height': 'Altezza', - 'Size': 'Dimensione', - 'Small': 'Piccolo', - 'Large': 'Largo', - 'Huge': 'Enorme', - 'Clear': 'Cancella', - 'Font': 'Font', - 'Search': 'Ricerca', - 'Camera': 'Camera', - 'Video': 'Video', - 'Undo': 'Undo', - 'Redo': 'Redo', - 'Font family': 'Font family', - 'Font size': 'Font size', - 'Bold': 'Bold', - 'Subscript': 'Subscript', - 'Superscript': 'Superscript', - 'Italic': 'Italic', - 'Underline': 'Underline', - 'Strike through': 'Strike through', - 'Inline code': 'Inline code', - 'Font color': 'Font color', - 'Background color': 'Background color', - 'Clear format': 'Clear format', - 'Align left': 'Align left', - 'Align center': 'Align center', - 'Align right': 'Align right', - 'Justify win width': 'Justify win width', - 'Text direction': 'Text direction', - 'Header style': 'Header style', - 'Numbered list': 'Numbered list', - 'Bullet list': 'Bullet list', - 'Checked list': 'Checked list', - 'Code block': 'Code block', - 'Quote': 'Quote', - 'Increase indent': 'Increase indent', - 'Decrease indent': 'Decrease indent', - 'Insert URL': 'Insert URL', - 'Visit link': 'Visit link', - 'Enter link': 'Enter link', - 'Enter media': 'Enter media', - 'Edit': 'Edit', - 'Apply': 'Apply', - 'Find text': 'Find text', - 'Move to previous occurrence': 'Move to previous occurrence', - 'Move to next occurrence': 'Move to next occurrence', - 'Saved using the network': 'Saved using the network', - 'Saved using the local storage': 'Saved using the local storage', - 'Error while saving image': 'Error while saving image', - 'Please enter a text for your link': "e.g., 'Learn more)", - 'Please enter the link url': "e.g., 'https://example.com'", - 'Please enter a valid image url': 'Please enter a valid image url' - }, - 'bn': { - 'Paste a link': 'লিঙ্ক পেস্ট করুন', - 'Ok': 'ওকে', - 'Select Color': 'কালার সিলেক্ট করুন', - 'Gallery': 'গ্যালারি', - 'Link': 'লিঙ্ক', - 'Open': 'ওপেন', - 'Copy': 'কপি', - 'Remove': 'রিমুভ', - 'Save': 'সেভ', - 'Zoom': 'জুম', - 'Saved': 'সেভড', - 'Text': 'টেক্সট', - 'Resize': 'রিসাইজ', - 'Width': 'প্রস্থ', - 'Height': 'দৈর্ঘ্য', - 'Size': 'সাইজ', - 'Small': 'ছোট', - 'Large': 'বড়', - 'Huge': 'বিশাল', - 'Clear': 'ক্লিয়ার', - 'Font': 'ফন্ট', - 'Search': 'সার্চ', - 'Camera': 'ক্যামেরা', - 'Video': 'ভিডিও', - 'Undo': 'আন্ডু', - 'Redo': 'রিডু', - 'Font family': 'ফন্ট ফ্যামিলি', - 'Font size': 'ফন্ট সাইজ', - 'Bold': 'বোল্ড', - 'Subscript': 'সাবস্ক্রিপ্ট', - 'Superscript': 'সুপারস্ক্রিপ্ট', - 'Italic': 'ইটালিক', - 'Underline': 'আন্ডারলাইন', - 'Strike through': 'স্ট্রাইক থ্রু', - 'Inline code': 'ইনলাইন কোড', - 'Font color': 'ফন্ট কালার', - 'Background color': 'ব্যাকগ্রাউন্ড কালার', - 'Clear format': 'ক্লিয়ার ফরম্যাট', - 'Align left': 'বাম সারিবদ্ধ', - 'Align center': 'কেন্দ্র সারিবদ্ধ', - 'Align right': 'ডান সারিবদ্ধ', - 'Justify win width': 'প্রস্থের সাথে সংযত', - 'Text direction': 'টেক্সট ডিরেকশন', - 'Header style': 'হেডার স্টাইল', - 'Numbered list': 'সংখ্যাযুক্ত তালিকা', - 'Bullet list': 'বুলেট তালিকা', - 'Checked list': 'চেক করা তালিকা', - 'Code block': 'কোড ব্লক', - 'Quote': 'উক্তি', - 'Increase indent': 'ইন্ডেন্ট বাড়ান', - 'Decrease indent': 'ইন্ডেন্ট কমান', - 'Insert URL': 'UR দিন', - 'Visit link': 'ভিজিট লিঙ্ক', - 'Enter link': 'লিঙ্ক দিন', - 'Enter media': 'মিডিয়া দিন', - 'Edit': 'ইডিট', - 'Apply': 'এপ্লাই', - 'Hex': 'হেক্স', - 'Material': 'ম্যাটারিয়াল', - 'Color': 'কালার', - 'Find text': 'Find text', - 'Move to previous occurrence': 'Move to previous occurrence', - 'Move to next occurrence': 'Move to next occurrence', - 'Saved using the network': 'Saved using the network', - 'Saved using the local storage': 'Saved using the local storage', - 'Error while saving image': 'Error while saving image', - 'Please enter a text for your link': "e.g., 'Learn more)", - 'Please enter the link url': "e.g., 'https://example.com'", - 'Please enter a valid image url': 'Please enter a valid image url' - }, - 'tk': { - 'Paste a link': 'Baglanyşygy goýuň', - 'Ok': 'Bolýar', - 'Select Color': 'Reňk saýlaň', - 'Gallery': 'Galereýa', - 'Link': 'Baglanyşyk', - 'Open': 'Aç', - 'Copy': 'Kopýala', - 'Remove': 'Poz', - 'Save': 'Sakla', - 'Zoom': 'Ulalt', - 'Saved': 'Saklandy', - 'Text': 'Tekst', - 'Resize': 'Ölçegini üýtget', - 'Width': 'In', - 'Height': 'Boý', - 'Size': 'Ölçegi', - 'Small': 'Kiçi', - 'Large': 'Uly', - 'Huge': 'Has uly', - 'Clear': 'Arassala', - 'Font': 'Şrift', - 'Search': 'Gözleg', - 'Camera': 'Kamera', - 'Video': 'Wideo', - 'Undo': 'Yza al', - 'Redo': 'Öňe al', - 'Font family': 'Şrift maşgalasy', - 'Font size': 'Şrift ululygy', - 'Bold': 'Galyň', - 'Subscript': 'Aşaky ýazgy', - 'Superscript': 'Ýokarky ýazgy', - 'Italic': 'Italik', - 'Underline': 'Aşagyny çyz', - 'Strike through': 'Üstüni çyz', - 'Inline code': 'Bir setirde kod', - 'Font color': 'Şrift reňki', - 'Background color': 'Arka reňki', - 'Clear format': 'Formaty arassala', - 'Align left': 'Çepe deňleşdir', - 'Align center': 'Orta deňleşdir', - 'Align right': 'Saga deňleşdir', - 'Justify win width': 'Justify win width', - 'Text direction': 'Tekst ugry', - 'Header style': 'Sözbaşy stili', - 'Numbered list': 'Sanly sanaw', - 'Bullet list': 'Okly sanawy', - 'Checked list': 'Tikli sanaw', - 'Code block': 'Kod blogy', - 'Quote': 'Sitata', - 'Increase indent': 'Indent köpelt', - 'Decrease indent': 'Indent azalt', - 'Insert URL': 'URL goý', - 'Visit link': 'Baglanyşyga giriň', - 'Enter link': 'Baglanyşyk giriň', - 'Enter media': 'Mediýa giriziň', - 'Edit': 'Üýtget', - 'Apply': 'Ulan', - 'Hex': 'Hex', - 'Material': 'Material', - 'Color': 'Reňk', - 'Find text': 'Tekst tapyň', - 'Move to previous occurrence': 'Öňki hadysa geçiň', - 'Move to next occurrence': 'Indiki hadysa geçiň', - 'Saved using the network': 'Saved using the network', - 'Saved using the local storage': 'Saved using the local storage', - 'Error while saving image': 'Error while saving image', - 'Please enter a text for your link': "e.g., 'Learn more)", - 'Please enter the link url': "e.g., 'https://example.com'", - 'Please enter a valid image url': 'Please enter a valid image url' - }, - 'bg': { - 'Paste a link': 'Поставете връзка', - 'Ok': 'Да', - 'Select Color': 'Изберете цвят', - 'Gallery': 'Галерия', - 'Link': 'Връзка', - 'Open': 'Отвори', - 'Copy': 'Копирай', - 'Remove': 'Премахни', - 'Save': 'Запази', - 'Zoom': 'Увеличи', - 'Saved': 'Запазено', - 'Text': 'Текст', - 'Resize': 'Промяна на размера', - 'Width': 'Ширина', - 'Height': 'Височина', - 'Size': 'Размер', - 'Small': 'Малък', - 'Large': 'Голям', - 'Huge': 'Огромен', - 'Clear': 'Изчисти', - 'Font': 'Шрифт', - 'Search': 'Търси', - 'Camera': 'Камера', - 'Video': 'Видео', - 'Undo': 'Отмени', - 'Redo': 'Възстанови', - 'Font family': 'Шрифт', - 'Font size': 'Размер на шрифта', - 'Bold': 'Получер', - 'Subscript': 'Индекс', - 'Superscript': 'Надпис', - 'Italic': 'Курсив', - 'Underline': 'Подчертан', - 'Strike through': 'Зачертан', - 'Inline code': 'Вграден код', - 'Font color': 'Цвят на шрифта', - 'Background color': 'Цвят на фона', - 'Clear format': 'Изчисти формат', - 'Align left': 'Подравни вляво', - 'Align center': 'Подравни в центъра', - 'Align right': 'Подравни вдясно', - 'Justify win width': 'Подравни във всяка колонка', - 'Text direction': 'Посока на текста', - 'Header style': 'Стил на заглавието', - 'Numbered list': 'Номериран списък', - 'Bullet list': 'Маркиран списък', - 'Checked list': 'Списък с отметки', - 'Code block': 'Блок с код', - 'Quote': 'Цитат', - 'Increase indent': 'Увеличи отстъпа', - 'Decrease indent': 'Намали отстъпа', - 'Insert URL': 'Вмъкни URL', - 'Visit link': 'Посети връзка', - 'Enter link': 'Въведи връзка', - 'Enter media': 'Въведи медия', - 'Edit': 'Редактирай', - 'Apply': 'Приложи', - 'Hex': 'Hex', - 'Material': 'Material', - 'Color': 'Цвят', - 'Find text': 'Намери текст', - 'Move to previous occurrence': 'Премести към предишното съвпадение', - 'Move to next occurrence': 'Премести към следващото съвпадение', - 'Saved using the network': 'Saved using the network', - 'Saved using the local storage': 'Saved using the local storage', - 'Error while saving image': 'Error while saving image', - 'Please enter a text for your link': "e.g., 'Learn more)", - 'Please enter the link url': "e.g., 'https://example.com'", - 'Please enter a valid image url': 'Please enter a valid image url' - }, - 'sw': { - 'Paste a link': 'Bandika Kiungo', - 'Ok': 'Sawa', - 'Select Color': 'Chagua Rangi', - 'Gallery': 'Matunzio', - 'Link': 'Kiungo', - 'Open': 'Fungua', - 'Copy': 'Nakili', - 'Remove': 'Ondoa', - 'Save': 'Hifadhi', - 'Zoom': 'Kuza', - 'Saved': 'Imehifadhiwa', - 'Text': 'Maandishi', - 'Resize': 'Badilisha Ukubwa', - 'Width': 'Upana', - 'Height': 'Urefu', - 'Size': 'Ukubwa', - 'Small': 'Ndogo', - 'Large': 'Kubwa', - 'Huge': 'Kubwa Sana', - 'Clear': 'Wazi', - 'Font': 'Fonti', - 'Search': 'Tafuta', - 'Camera': 'Kamera', - 'Video': 'Video', - 'Undo': 'Fanyua', - 'Redo': 'Fanya Upya', - 'Font family': 'Familia ya Fonti', - 'Font size': 'Ukubwa wa Fonti', - 'Bold': 'Nono', - 'Subscript': 'Maandishi ys Chini', - 'Superscript': 'Maandishi ya Juu', - 'Italic': 'Italiki', - 'Underline': 'Pigia Mstari', - 'Strike through': 'Ghairi Maandishi', - 'Inline code': 'Codi ya Laini Moja', - 'Font color': 'Rangi ya Fonti', - 'Background color': 'Rangi ya Nyuma', - 'Clear format': 'Muundo Wazi', - 'Align left': 'Pangilia Kushoto', - 'Align center': 'Pangilia Kati', - 'Align right': 'Pangilia Kulia', - 'Justify win width': 'Kuhalalisha Upana wa Ushindi', - 'Text direction': 'Mwelekeo wa Maandishi', - 'Header style': 'Mtindo wa Mada', - 'Numbered list': 'Orodha ya Nambari', - 'Bullet list': 'Orodha ya Risasi', - 'Checked list': 'Orodha iliyoangaliwa', - 'Code block': 'aya ya codi', - 'Quote': 'Nukuu', - 'Increase indent': 'Ongeza Ujongezaji', - 'Decrease indent': 'Punguza Ujongezaji', - 'Insert URL': 'Ingiza Kiungo', - 'Visit link': 'Tembelea Kiungo', - 'Enter link': 'Ingiza Kiungo', - 'Enter media': 'Ingiza Picha', - 'Edit': 'Harir', - 'Apply': 'Weka', - 'Hex': 'Hexi', - 'Material': 'Nyenzo', - 'Color': 'Rangi', - 'Find text': 'Pata Maandishi', - 'Move to previous occurrence': 'Nenda Kwenye Tukio la Awali', - 'Move to next occurrence': 'Nenda kwa Tukio linalofuata', - 'Saved using the network': 'Imehifadhiwa kwa Kutumia Mtandao', - 'Saved using the local storage': 'Imehifadhiwa kwa Hifadhi ya Ndani', - 'Error while saving image': 'Hitilafu Wakati wa Kuhifadhi Picha', - 'Please enter a text for your link': "e.g., 'Learn more)", - 'Please enter the link url': "e.g., 'https://example.com'", - 'Please enter a valid image url': 'Please enter a valid image url' - }, - }; - - String get i18n => localize(this, _t); -} diff --git a/lib/src/utils/color.dart b/lib/src/utils/color.dart index 3dc5d07a..2a746e80 100644 --- a/lib/src/utils/color.dart +++ b/lib/src/utils/color.dart @@ -118,7 +118,7 @@ Color stringToColor(String? s, [Color? originalColor]) { } if (!s.startsWith('#')) { - throw 'Color code not supported'; + throw UnsupportedError('Color code not supported'); } var hex = s.replaceFirst('#', ''); diff --git a/lib/src/utils/delta.dart b/lib/src/utils/delta.dart index c737e1ac..5c244e01 100644 --- a/lib/src/utils/delta.dart +++ b/lib/src/utils/delta.dart @@ -1,13 +1,20 @@ import 'dart:math' as math; import 'dart:ui'; +import 'package:meta/meta.dart' show immutable; + import '../models/documents/attribute.dart'; import '../models/documents/nodes/node.dart'; import '../models/quill_delta.dart'; // Diff between two texts - old text and new text +@immutable class Diff { - Diff(this.start, this.deleted, this.inserted); + const Diff({ + required this.start, + required this.deleted, + required this.inserted, + }); // Start index in old text at which changes begin. final int start; @@ -37,7 +44,11 @@ Diff getDiff(String oldText, String newText, int cursorPosition) { start++) {} final deleted = (start >= end) ? '' : oldText.substring(start, end); final inserted = newText.substring(start, end + delta); - return Diff(start, deleted, inserted); + return Diff( + start: start, + deleted: deleted, + inserted: inserted, + ); } int getPositionDelta(Delta user, Delta actual) { @@ -53,8 +64,10 @@ int getPositionDelta(Delta user, Delta actual) { final userOperation = userItr.next(length); final actualOperation = actualItr.next(length); if (userOperation.length != actualOperation.length) { - throw 'userOp ${userOperation.length} does not match actualOp ' - '${actualOperation.length}'; + throw ArgumentError( + 'userOp ${userOperation.length} does not match actualOp ' + '${actualOperation.length}', + ); } if (userOperation.key == actualOperation.key) { continue; diff --git a/lib/src/utils/embeds.dart b/lib/src/utils/embeds.dart index db693ba8..7164f1d5 100644 --- a/lib/src/utils/embeds.dart +++ b/lib/src/utils/embeds.dart @@ -7,7 +7,7 @@ import '../widgets/controller.dart'; OffsetValue getEmbedNode(QuillController controller, int offset) { var offset = controller.selection.start; var embedNode = controller.queryNode(offset); - if (embedNode == null || !(embedNode is Embed)) { + if (embedNode == null || embedNode is! Embed) { offset = max(0, offset - 1); embedNode = controller.queryNode(offset); } @@ -15,5 +15,5 @@ OffsetValue getEmbedNode(QuillController controller, int offset) { return OffsetValue(offset, embedNode); } - return throw 'Embed node not found by offset $offset'; + return throw ArgumentError('Embed node not found by offset $offset'); } diff --git a/lib/src/utils/experimental.dart b/lib/src/utils/experimental.dart deleted file mode 100644 index 8f24e87e..00000000 --- a/lib/src/utils/experimental.dart +++ /dev/null @@ -1,7 +0,0 @@ -import 'package:flutter/foundation.dart' show immutable; - -@immutable -class Experimental { - const Experimental([this.reason = 'Experimental feature']); - final String reason; -} diff --git a/lib/src/utils/font.dart b/lib/src/utils/font.dart index 1e996e10..5d1aa018 100644 --- a/lib/src/utils/font.dart +++ b/lib/src/utils/font.dart @@ -15,7 +15,7 @@ dynamic getFontSize(dynamic sizeValue) { assert(sizeValue is String); final fontSize = double.tryParse(sizeValue); if (fontSize == null) { - throw 'Invalid size $sizeValue'; + throw ArgumentError('Invalid size $sizeValue'); } return fontSize; } diff --git a/lib/src/utils/platform.dart b/lib/src/utils/platform.dart index 73a89fcb..d248b369 100644 --- a/lib/src/utils/platform.dart +++ b/lib/src/utils/platform.dart @@ -2,64 +2,118 @@ import 'dart:io' show Platform; import 'package:device_info_plus/device_info_plus.dart'; import 'package:flutter/foundation.dart' - show kIsWeb, TargetPlatform, defaultTargetPlatform; + show TargetPlatform, defaultTargetPlatform, kIsWeb, visibleForTesting; -bool isWeb() { - return kIsWeb; +/// If you want to override the [kIsWeb] use [overrideIsWeb] but it's only +/// for testing +bool isWeb({ + @visibleForTesting bool? overrideIsWeb, +}) { + return overrideIsWeb ?? kIsWeb; } -bool isMobile([TargetPlatform? targetPlatform]) { - if (isWeb()) return false; - targetPlatform ??= defaultTargetPlatform; - return {TargetPlatform.iOS, TargetPlatform.android}.contains(targetPlatform); +/// [supportWeb] is a parameter that ask you if we should care about web support +/// if the value is true then we will return the result no matter if we are +/// on web or using a native app to run the flutter app +bool isMobile({ + required bool supportWeb, + TargetPlatform? platform, + bool? overrideIsWeb, +}) { + if (isWeb(overrideIsWeb: overrideIsWeb) && !supportWeb) return false; + platform ??= defaultTargetPlatform; + return {TargetPlatform.iOS, TargetPlatform.android}.contains(platform); } -bool isDesktop([TargetPlatform? targetPlatform]) { - if (isWeb()) return false; - targetPlatform ??= defaultTargetPlatform; +/// [supportWeb] is a parameter that ask you if we should care about web support +/// if the value is true then we will return the result no matter if we are +/// on web or using a native app to run the flutter app +bool isDesktop({ + required bool supportWeb, + TargetPlatform? platform, + bool? overrideIsWeb, +}) { + if (isWeb(overrideIsWeb: overrideIsWeb) && !supportWeb) return false; + platform ??= defaultTargetPlatform; return {TargetPlatform.macOS, TargetPlatform.linux, TargetPlatform.windows} - .contains(targetPlatform); + .contains(platform); } -bool isKeyboardOS([TargetPlatform? targetPlatform]) { - targetPlatform ??= defaultTargetPlatform; - return isDesktop(targetPlatform) || targetPlatform == TargetPlatform.fuchsia; +/// [supportWeb] is a parameter that ask you if we should care about web support +/// if the value is true then we will return the result no matter if we are +/// on web or using a native app to run the flutter app +bool isKeyboardOS({ + required bool supportWeb, + TargetPlatform? platform, + bool? overrideIsWeb, +}) { + platform ??= defaultTargetPlatform; + return isDesktop( + platform: platform, + supportWeb: supportWeb, + overrideIsWeb: overrideIsWeb) || + platform == TargetPlatform.fuchsia; } -bool isAppleOS([TargetPlatform? targetPlatform]) { - if (isWeb()) return false; - targetPlatform ??= defaultTargetPlatform; +/// [supportWeb] is a parameter that ask you if we should care about web support +/// if the value is true then we will return the result no matter if we are +/// on web or using a native app to run the flutter app +bool isAppleOS({ + required bool supportWeb, + TargetPlatform? platform, + bool? overrideIsWeb, +}) { + if (isWeb(overrideIsWeb: overrideIsWeb) && !supportWeb) return false; + platform ??= defaultTargetPlatform; return { TargetPlatform.macOS, TargetPlatform.iOS, - }.contains(targetPlatform); + }.contains(platform); } -bool isMacOS([TargetPlatform? targetPlatform]) { - if (isWeb()) return false; - targetPlatform ??= defaultTargetPlatform; - return TargetPlatform.macOS == targetPlatform; +/// [supportWeb] is a parameter that ask you if we should care about web support +/// if the value is true then we will return the result no matter if we are +/// on web or using a native app to run the flutter app +bool isMacOS({ + required bool supportWeb, + TargetPlatform? platform, + bool? overrideIsWeb, +}) { + if (isWeb(overrideIsWeb: overrideIsWeb) && !supportWeb) return false; + platform ??= defaultTargetPlatform; + return TargetPlatform.macOS == platform; } -bool isIOS([TargetPlatform? targetPlatform]) { - if (isWeb()) return false; - targetPlatform ??= defaultTargetPlatform; - return TargetPlatform.iOS == targetPlatform; +/// [supportWeb] is a parameter that ask you if we should care about web support +/// if the value is true then we will return the result no matter if we are +/// on web or using a native app to run the flutter app +bool isIOS({ + required bool supportWeb, + TargetPlatform? platform, + bool? overrideIsWeb, +}) { + if (isWeb(overrideIsWeb: overrideIsWeb) && !supportWeb) return false; + platform ??= defaultTargetPlatform; + return TargetPlatform.iOS == platform; } -bool isAndroid([TargetPlatform? targetPlatform]) { - if (isWeb()) return false; - targetPlatform ??= defaultTargetPlatform; - return TargetPlatform.android == targetPlatform; +/// [supportWeb] is a parameter that ask you if we should care about web support +/// if the value is true then we will return the result no matter if we are +/// on web or using a native app to run the flutter app +bool isAndroid({ + required bool supportWeb, + TargetPlatform? platform, + bool? overrideIsWeb, +}) { + if (isWeb(overrideIsWeb: overrideIsWeb) && !supportWeb) return false; + platform ??= defaultTargetPlatform; + return TargetPlatform.android == platform; } -bool isFlutterTest() { - if (isWeb()) return false; - return Platform.environment.containsKey('FLUTTER_TEST'); -} - -Future isIOSSimulator() async { - if (!isAppleOS()) { +Future isIOSSimulator({ + bool? overrideIsWeb, +}) async { + if (!isAppleOS(supportWeb: false, overrideIsWeb: overrideIsWeb)) { return false; } @@ -73,3 +127,10 @@ Future isIOSSimulator() async { } return false; } + +bool isFlutterTest({ + bool? overrideIsWeb, +}) { + if (isWeb(overrideIsWeb: overrideIsWeb)) return false; + return Platform.environment.containsKey('FLUTTER_TEST'); +} diff --git a/lib/src/utils/string.dart b/lib/src/utils/string.dart index f639c716..529ec24d 100644 --- a/lib/src/utils/string.dart +++ b/lib/src/utils/string.dart @@ -6,53 +6,40 @@ Map parseKeyValuePairs(String s, Set targetKeys) { final result = {}; final pairs = s.split(';'); for (final pair in pairs) { - final _index = pair.indexOf(':'); - if (_index < 0) { + final index = pair.indexOf(':'); + if (index < 0) { continue; } - final _key = pair.substring(0, _index).trim(); - if (targetKeys.contains(_key)) { - result[_key] = pair.substring(_index + 1).trim(); + final key = pair.substring(0, index).trim(); + if (targetKeys.contains(key)) { + result[key] = pair.substring(index + 1).trim(); } } return result; } -@Deprecated('Use replaceStyleStringWithSize instead') -String replaceStyleString( - String s, - double width, - double height, -) { - return replaceStyleStringWithSize( - s, - width: width, - height: height, - isMobile: true, - ); -} - +@Deprecated('This function is no longer used in flutter_quill') String replaceStyleStringWithSize( - String s, { + String cssStyle, { required double width, required double height, required bool isMobile, }) { final result = {}; - final pairs = s.split(';'); + final pairs = cssStyle.split(';'); for (final pair in pairs) { - final _index = pair.indexOf(':'); - if (_index < 0) { + final index = pair.indexOf(':'); + if (index < 0) { continue; } - final _key = pair.substring(0, _index).trim(); - result[_key] = pair.substring(_index + 1).trim(); + final key = pair.substring(0, index).trim(); + result[key] = pair.substring(index + 1).trim(); } if (isMobile) { - result[Attribute.mobileWidth] = width.toString(); - result[Attribute.mobileHeight] = height.toString(); + result['mobileWidth'] = width.toString(); + result['mobileHeight'] = height.toString(); } else { result[Attribute.width.key] = width.toString(); result[Attribute.height.key] = height.toString(); @@ -68,13 +55,14 @@ String replaceStyleStringWithSize( return sb.toString(); } -Alignment getAlignment(String? s) { - const _defaultAlignment = Alignment.center; - if (s == null) { - return _defaultAlignment; +/// Get flutter [Alignment] value by [cssAlignment] +Alignment getAlignment(String? cssAlignment) { + const defaultAlignment = Alignment.center; + if (cssAlignment == null) { + return defaultAlignment; } - final _index = [ + final index = [ 'topLeft', 'topCenter', 'topRight', @@ -84,9 +72,9 @@ Alignment getAlignment(String? s) { 'bottomLeft', 'bottomCenter', 'bottomRight' - ].indexOf(s); - if (_index < 0) { - return _defaultAlignment; + ].indexOf(cssAlignment); + if (index < 0) { + return defaultAlignment; } return [ @@ -99,5 +87,5 @@ Alignment getAlignment(String? s) { Alignment.bottomLeft, Alignment.bottomCenter, Alignment.bottomRight - ][_index]; + ][index]; } diff --git a/lib/src/utils/widgets.dart b/lib/src/utils/widgets.dart index f7126616..0ac7c9ac 100644 --- a/lib/src/utils/widgets.dart +++ b/lib/src/utils/widgets.dart @@ -3,7 +3,9 @@ import 'package:flutter/material.dart'; typedef WidgetWrapper = Widget Function(Widget child); /// Provides utiulity widgets. -abstract class UtilityWidgets { +class UtilityWidgets { + const UtilityWidgets._(); + /// Conditionally wraps the [child] with [Tooltip] widget if [message] /// is not null and not empty. static Widget maybeTooltip({ diff --git a/lib/src/widgets/controller.dart b/lib/src/widgets/controller.dart index 242faa55..b48ab9eb 100644 --- a/lib/src/widgets/controller.dart +++ b/lib/src/widgets/controller.dart @@ -202,7 +202,7 @@ class QuillController extends ChangeNotifier { // TextSelection.collapsed(offset: document.length), ChangeSource.LOCAL); updateSelection( TextSelection.collapsed(offset: selection.baseOffset + len), - ChangeSource.LOCAL); + ChangeSource.local); } else { // no need to move cursor notifyListeners(); @@ -261,13 +261,13 @@ class QuillController extends ChangeNotifier { final retainDelta = Delta() ..retain(index) ..retain(data is String ? data.length : 1, toggledStyle.toJson()); - document.compose(retainDelta, ChangeSource.LOCAL); + document.compose(retainDelta, ChangeSource.local); } } if (textSelection != null) { if (delta == null || delta.isEmpty) { - _updateSelection(textSelection, ChangeSource.LOCAL); + _updateSelection(textSelection, ChangeSource.local); } else { final user = Delta() ..retain(index) @@ -279,7 +279,7 @@ class QuillController extends ChangeNotifier { baseOffset: textSelection.baseOffset + positionDelta, extentOffset: textSelection.extentOffset + positionDelta, ), - ChangeSource.LOCAL, + ChangeSource.local, ); } } @@ -322,7 +322,7 @@ class QuillController extends ChangeNotifier { baseOffset: change.transformPosition(selection.baseOffset), extentOffset: change.transformPosition(selection.extentOffset)); if (selection != adjustedSelection) { - _updateSelection(adjustedSelection, ChangeSource.LOCAL); + _updateSelection(adjustedSelection, ChangeSource.local); } notifyListeners(); } @@ -333,18 +333,23 @@ class QuillController extends ChangeNotifier { void moveCursorToStart() { updateSelection( - const TextSelection.collapsed(offset: 0), ChangeSource.LOCAL); + const TextSelection.collapsed(offset: 0), + ChangeSource.local, + ); } void moveCursorToPosition(int position) { updateSelection( - TextSelection.collapsed(offset: position), ChangeSource.LOCAL); + TextSelection.collapsed(offset: position), + ChangeSource.local, + ); } void moveCursorToEnd() { updateSelection( - TextSelection.collapsed(offset: plainTextEditingValue.text.length), - ChangeSource.LOCAL); + TextSelection.collapsed(offset: plainTextEditingValue.text.length), + ChangeSource.local, + ); } void updateSelection(TextSelection textSelection, ChangeSource source) { @@ -358,9 +363,12 @@ class QuillController extends ChangeNotifier { } textSelection = selection.copyWith( - baseOffset: delta.transformPosition(selection.baseOffset, force: false), - extentOffset: - delta.transformPosition(selection.extentOffset, force: false)); + baseOffset: delta.transformPosition(selection.baseOffset, force: false), + extentOffset: delta.transformPosition( + selection.extentOffset, + force: false, + ), + ); if (selection != textSelection) { _updateSelection(textSelection, source); } diff --git a/lib/src/widgets/cursor.dart b/lib/src/widgets/cursor.dart index de2dbb5f..929118b1 100644 --- a/lib/src/widgets/cursor.dart +++ b/lib/src/widgets/cursor.dart @@ -257,7 +257,11 @@ class CursorPainter { /// [offset] is global top left (x, y) of text line /// [position] is relative (x) in text line void paint( - Canvas canvas, Offset offset, TextPosition position, bool lineHasEmbed) { + Canvas canvas, + Offset offset, + TextPosition position, + bool lineHasEmbed, + ) { // relative (x, y) to global offset var relativeCaretOffset = editable!.getOffsetForCaret(position, prototype); if (lineHasEmbed && relativeCaretOffset == Offset.zero) { @@ -287,7 +291,7 @@ class CursorPainter { final caretHeight = editable!.getFullHeightForCaret(position); if (caretHeight != null) { - if (isAppleOS()) { + if (isAppleOS(supportWeb: true)) { // Center the caret vertically along the text. caretRect = Rect.fromLTWH( caretRect.left, diff --git a/lib/src/widgets/default_styles.dart b/lib/src/widgets/default_styles.dart index d600b71d..5d5949f8 100644 --- a/lib/src/widgets/default_styles.dart +++ b/lib/src/widgets/default_styles.dart @@ -11,9 +11,9 @@ import 'style_widgets/checkbox_point.dart'; class QuillStyles extends InheritedWidget { const QuillStyles({ required this.data, - required Widget child, - Key? key, - }) : super(key: key, child: child); + required super.child, + super.key, + }); final DefaultStyles data; @@ -128,12 +128,12 @@ class InlineCodeStyle { @immutable class DefaultListBlockStyle extends DefaultTextBlockStyle { const DefaultListBlockStyle( - TextStyle style, - VerticalSpacing verticalSpacing, - VerticalSpacing lineSpacing, - BoxDecoration? decoration, + super.style, + super.verticalSpacing, + super.lineSpacing, + super.decoration, this.checkboxUIBuilder, - ) : super(style, verticalSpacing, lineSpacing, decoration); + ); final QuillCheckboxBuilder? checkboxUIBuilder; } @@ -204,7 +204,7 @@ class DefaultStyles { ); const baseSpacing = VerticalSpacing(6, 0); String fontFamily; - if (isAppleOS(themeData.platform)) { + if (isAppleOS(platform: themeData.platform, supportWeb: true)) { fontFamily = 'Menlo'; } else { fontFamily = 'Roboto Mono'; @@ -240,22 +240,30 @@ class DefaultStyles { const VerticalSpacing(0, 0), null), h3: DefaultTextBlockStyle( - defaultTextStyle.style.copyWith( - fontSize: 20, - color: defaultTextStyle.style.color!.withOpacity(0.70), - height: 1.25, - fontWeight: FontWeight.w500, - decoration: TextDecoration.none, - ), - const VerticalSpacing(8, 0), - const VerticalSpacing(0, 0), - null), + defaultTextStyle.style.copyWith( + fontSize: 20, + color: defaultTextStyle.style.color!.withOpacity(0.70), + height: 1.25, + fontWeight: FontWeight.w500, + decoration: TextDecoration.none, + ), + const VerticalSpacing(8, 0), + const VerticalSpacing(0, 0), + null, + ), paragraph: DefaultTextBlockStyle(baseStyle, const VerticalSpacing(0, 0), const VerticalSpacing(0, 0), null), bold: const TextStyle(fontWeight: FontWeight.bold), - subscript: const TextStyle(fontFeatures: [FontFeature.subscripts()]), - superscript: - const TextStyle(fontFeatures: [FontFeature.superscripts()]), + subscript: const TextStyle( + fontFeatures: [ + FontFeature.subscripts(), + ], + ), + superscript: const TextStyle( + fontFeatures: [ + FontFeature.superscripts(), + ], + ), italic: const TextStyle(fontStyle: FontStyle.italic), small: const TextStyle(fontSize: 12), underline: const TextStyle(decoration: TextDecoration.underline), @@ -288,16 +296,22 @@ class DefaultStyles { const VerticalSpacing(0, 0), null), lists: DefaultListBlockStyle( - baseStyle, baseSpacing, const VerticalSpacing(0, 6), null, null), + baseStyle, + baseSpacing, + const VerticalSpacing(0, 6), + null, + null, + ), quote: DefaultTextBlockStyle( - TextStyle(color: baseStyle.color!.withOpacity(0.6)), - baseSpacing, - const VerticalSpacing(6, 2), - BoxDecoration( - border: Border( - left: BorderSide(width: 4, color: Colors.grey.shade300), - ), - )), + TextStyle(color: baseStyle.color!.withOpacity(0.6)), + baseSpacing, + const VerticalSpacing(6, 2), + BoxDecoration( + border: Border( + left: BorderSide(width: 4, color: Colors.grey.shade300), + ), + ), + ), code: DefaultTextBlockStyle( TextStyle( color: Colors.blue.shade900.withOpacity(0.9), @@ -312,11 +326,23 @@ class DefaultStyles { borderRadius: BorderRadius.circular(2), )), indent: DefaultTextBlockStyle( - baseStyle, baseSpacing, const VerticalSpacing(0, 6), null), - align: DefaultTextBlockStyle(baseStyle, const VerticalSpacing(0, 0), - const VerticalSpacing(0, 0), null), - leading: DefaultTextBlockStyle(baseStyle, const VerticalSpacing(0, 0), - const VerticalSpacing(0, 0), null), + baseStyle, + baseSpacing, + const VerticalSpacing(0, 6), + null, + ), + align: DefaultTextBlockStyle( + baseStyle, + const VerticalSpacing(0, 0), + const VerticalSpacing(0, 0), + null, + ), + leading: DefaultTextBlockStyle( + baseStyle, + const VerticalSpacing(0, 0), + const VerticalSpacing(0, 0), + null, + ), sizeSmall: const TextStyle(fontSize: 10), sizeLarge: const TextStyle(fontSize: 18), sizeHuge: const TextStyle(fontSize: 22)); @@ -324,29 +350,30 @@ class DefaultStyles { DefaultStyles merge(DefaultStyles other) { return DefaultStyles( - h1: other.h1 ?? h1, - h2: other.h2 ?? h2, - h3: other.h3 ?? h3, - paragraph: other.paragraph ?? paragraph, - bold: other.bold ?? bold, - subscript: other.subscript ?? subscript, - superscript: other.superscript ?? superscript, - italic: other.italic ?? italic, - small: other.small ?? small, - underline: other.underline ?? underline, - strikeThrough: other.strikeThrough ?? strikeThrough, - inlineCode: other.inlineCode ?? inlineCode, - link: other.link ?? link, - color: other.color ?? color, - placeHolder: other.placeHolder ?? placeHolder, - lists: other.lists ?? lists, - quote: other.quote ?? quote, - code: other.code ?? code, - indent: other.indent ?? indent, - align: other.align ?? align, - leading: other.leading ?? leading, - sizeSmall: other.sizeSmall ?? sizeSmall, - sizeLarge: other.sizeLarge ?? sizeLarge, - sizeHuge: other.sizeHuge ?? sizeHuge); + h1: other.h1 ?? h1, + h2: other.h2 ?? h2, + h3: other.h3 ?? h3, + paragraph: other.paragraph ?? paragraph, + bold: other.bold ?? bold, + subscript: other.subscript ?? subscript, + superscript: other.superscript ?? superscript, + italic: other.italic ?? italic, + small: other.small ?? small, + underline: other.underline ?? underline, + strikeThrough: other.strikeThrough ?? strikeThrough, + inlineCode: other.inlineCode ?? inlineCode, + link: other.link ?? link, + color: other.color ?? color, + placeHolder: other.placeHolder ?? placeHolder, + lists: other.lists ?? lists, + quote: other.quote ?? quote, + code: other.code ?? code, + indent: other.indent ?? indent, + align: other.align ?? align, + leading: other.leading ?? leading, + sizeSmall: other.sizeSmall ?? sizeSmall, + sizeLarge: other.sizeLarge ?? sizeLarge, + sizeHuge: other.sizeHuge ?? sizeHuge, + ); } } diff --git a/lib/src/widgets/delegate.dart b/lib/src/widgets/delegate.dart index c196a5d3..5d021606 100644 --- a/lib/src/widgets/delegate.dart +++ b/lib/src/widgets/delegate.dart @@ -314,8 +314,10 @@ class EditorTextSelectionGestureDetectorBuilder { void onDragSelectionUpdate( //DragStartDetails startDetails, DragUpdateDetails updateDetails) { - renderEditor!.extendSelection(updateDetails.globalPosition, - cause: SelectionChangedCause.drag); + renderEditor!.extendSelection( + updateDetails.globalPosition, + cause: SelectionChangedCause.drag, + ); } /// Handler for [EditorTextSelectionGestureDetector.onDragSelectionEnd]. @@ -329,7 +331,9 @@ class EditorTextSelectionGestureDetectorBuilder { @protected void onDragSelectionEnd(DragEndDetails details) { renderEditor!.handleDragEnd(details); - if (isDesktop() && + // TODO: Should we care if the platform is desktop using native desktop app + // or the flutter app is running using web app?? + if (isDesktop(supportWeb: true) && delegate.selectionEnabled && shouldShowSelectionToolbar) { // added to show selection copy/paste toolbar after drag to select @@ -341,29 +345,30 @@ class EditorTextSelectionGestureDetectorBuilder { /// the handlers provided by this builder. /// /// The [child] or its subtree should contain [EditableText]. - Widget build( - {required HitTestBehavior behavior, - required Widget child, - Key? key, - bool detectWordBoundary = true}) { + Widget build({ + required HitTestBehavior behavior, + required Widget child, + Key? key, + bool detectWordBoundary = true, + }) { return EditorTextSelectionGestureDetector( - key: key, - onTapDown: onTapDown, - onForcePressStart: - delegate.forcePressEnabled ? onForcePressStart : null, - onForcePressEnd: delegate.forcePressEnabled ? onForcePressEnd : null, - onSingleTapUp: onSingleTapUp, - onSingleTapCancel: onSingleTapCancel, - onSingleLongTapStart: onSingleLongTapStart, - onSingleLongTapMoveUpdate: onSingleLongTapMoveUpdate, - onSingleLongTapEnd: onSingleLongTapEnd, - onDoubleTapDown: onDoubleTapDown, - onSecondarySingleTapUp: onSecondarySingleTapUp, - onDragSelectionStart: onDragSelectionStart, - onDragSelectionUpdate: onDragSelectionUpdate, - onDragSelectionEnd: onDragSelectionEnd, - behavior: behavior, - detectWordBoundary: detectWordBoundary, - child: child); + key: key, + onTapDown: onTapDown, + onForcePressStart: delegate.forcePressEnabled ? onForcePressStart : null, + onForcePressEnd: delegate.forcePressEnabled ? onForcePressEnd : null, + onSingleTapUp: onSingleTapUp, + onSingleTapCancel: onSingleTapCancel, + onSingleLongTapStart: onSingleLongTapStart, + onSingleLongTapMoveUpdate: onSingleLongTapMoveUpdate, + onSingleLongTapEnd: onSingleLongTapEnd, + onDoubleTapDown: onDoubleTapDown, + onSecondarySingleTapUp: onSecondarySingleTapUp, + onDragSelectionStart: onDragSelectionStart, + onDragSelectionUpdate: onDragSelectionUpdate, + onDragSelectionEnd: onDragSelectionEnd, + behavior: behavior, + detectWordBoundary: detectWordBoundary, + child: child, + ); } } diff --git a/lib/src/widgets/editor/editor.dart b/lib/src/widgets/editor/editor.dart index 766ad8aa..b637dab1 100644 --- a/lib/src/widgets/editor/editor.dart +++ b/lib/src/widgets/editor/editor.dart @@ -1,31 +1,26 @@ import 'dart:math' as math; -// ignore: unnecessary_import -// import 'dart:typed_data'; -// The project maanged to compiled successfully without the import -// do we still need this import (dart:typed_data)?? - -import 'package:flutter/cupertino.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/gestures.dart'; +import 'package:flutter/cupertino.dart' + show CupertinoTheme, cupertinoTextSelectionControls; +import 'package:flutter/foundation.dart' show ValueListenable; +import 'package:flutter/gestures.dart' show PointerDeviceKind; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; -import 'package:i18n_extension/i18n_widget.dart'; import '../../../flutter_quill.dart'; +import '../../l10n/widgets/localizations.dart'; import '../../models/documents/nodes/container.dart' as container_node; import '../../utils/platform.dart'; import '../box.dart'; -import '../cursor.dart'; import '../delegate.dart'; import '../float_cursor.dart'; -import '../raw_editor/raw_editor.dart'; import '../text_selection.dart'; +import 'editor_builder.dart'; /// Base interface for the editor state which defines contract used by /// various mixins. -abstract class EditorState extends State +abstract class EditorState extends State implements TextSelectionDelegate { ScrollController get scrollController; @@ -219,7 +214,10 @@ class QuillEditorState extends State Color selectionColor; Radius? cursorRadius; - if (isAppleOS(theme.platform)) { + if (isAppleOS( + platform: theme.platform, + supportWeb: true, + )) { final cupertinoTheme = CupertinoTheme.of(context); textSelectionControls = cupertinoTextSelectionControls; paintCursorAboveText = true; @@ -242,74 +240,83 @@ class QuillEditorState extends State final showSelectionToolbar = configurations.enableInteractiveSelection && configurations.enableSelectionToolbar; - final child = QuillEditorProvider( - editorConfigurations: configurations, - child: RawEditor( - key: _editorKey, - controller: context.requireQuillController, - focusNode: widget.focusNode, - scrollController: widget.scrollController, - scrollable: configurations.scrollable, - scrollBottomInset: configurations.scrollBottomInset, - padding: configurations.padding, - readOnly: configurations.readOnly, - placeholder: configurations.placeholder, - onLaunchUrl: configurations.onLaunchUrl, - contextMenuBuilder: showSelectionToolbar - ? (configurations.contextMenuBuilder ?? - RawEditor.defaultContextMenuBuilder) - : null, - showSelectionHandles: isMobile(theme.platform), - showCursor: configurations.showCursor, - cursorStyle: CursorStyle( - color: cursorColor, - backgroundColor: Colors.grey, - width: 2, - radius: cursorRadius, - offset: cursorOffset, - paintAboveText: - configurations.paintCursorAboveText ?? paintCursorAboveText, - opacityAnimates: cursorOpacityAnimates, + final child = FlutterQuillLocalizationsWidget( + child: QuillEditorProvider( + editorConfigurations: configurations, + child: QuillEditorBuilderWidget( + builder: configurations.builder, + child: QuillRawEditor( + key: _editorKey, + configurations: QuillRawEditorConfigurations( + controller: context.requireQuillController, + focusNode: widget.focusNode, + scrollController: widget.scrollController, + scrollable: configurations.scrollable, + scrollBottomInset: configurations.scrollBottomInset, + padding: configurations.padding, + isReadOnly: configurations.readOnly, + placeholder: configurations.placeholder, + onLaunchUrl: configurations.onLaunchUrl, + contextMenuBuilder: showSelectionToolbar + ? (configurations.contextMenuBuilder ?? + QuillRawEditorConfigurations.defaultContextMenuBuilder) + : null, + showSelectionHandles: isMobile( + platform: theme.platform, + supportWeb: true, + ), + showCursor: configurations.showCursor ?? true, + cursorStyle: CursorStyle( + color: cursorColor, + backgroundColor: Colors.grey, + width: 2, + radius: cursorRadius, + offset: cursorOffset, + paintAboveText: + configurations.paintCursorAboveText ?? paintCursorAboveText, + opacityAnimates: cursorOpacityAnimates, + ), + textCapitalization: configurations.textCapitalization, + minHeight: configurations.minHeight, + maxHeight: configurations.maxHeight, + maxContentWidth: configurations.maxContentWidth, + customStyles: configurations.customStyles, + expands: configurations.expands, + autoFocus: configurations.autoFocus, + selectionColor: selectionColor, + selectionCtrls: + configurations.textSelectionControls ?? textSelectionControls, + keyboardAppearance: configurations.keyboardAppearance, + enableInteractiveSelection: + configurations.enableInteractiveSelection, + scrollPhysics: configurations.scrollPhysics, + embedBuilder: _getEmbedBuilder, + linkActionPickerDelegate: configurations.linkActionPickerDelegate, + customStyleBuilder: configurations.customStyleBuilder, + customRecognizerBuilder: configurations.customRecognizerBuilder, + floatingCursorDisabled: configurations.floatingCursorDisabled, + onImagePaste: configurations.onImagePaste, + customShortcuts: configurations.customShortcuts, + customActions: configurations.customActions, + customLinkPrefixes: configurations.customLinkPrefixes, + isOnTapOutsideEnabled: configurations.isOnTapOutsideEnabled, + onTapOutside: configurations.onTapOutside, + dialogTheme: configurations.dialogTheme, + contentInsertionConfiguration: + configurations.contentInsertionConfiguration, + ), + ), ), - textCapitalization: configurations.textCapitalization, - minHeight: configurations.minHeight, - maxHeight: configurations.maxHeight, - maxContentWidth: configurations.maxContentWidth, - customStyles: configurations.customStyles, - expands: configurations.expands, - autoFocus: configurations.autoFocus, - selectionColor: selectionColor, - selectionCtrls: - configurations.textSelectionControls ?? textSelectionControls, - keyboardAppearance: configurations.keyboardAppearance, - enableInteractiveSelection: configurations.enableInteractiveSelection, - scrollPhysics: configurations.scrollPhysics, - embedBuilder: _getEmbedBuilder, - linkActionPickerDelegate: configurations.linkActionPickerDelegate, - customStyleBuilder: configurations.customStyleBuilder, - customRecognizerBuilder: configurations.customRecognizerBuilder, - floatingCursorDisabled: configurations.floatingCursorDisabled, - onImagePaste: configurations.onImagePaste, - customShortcuts: configurations.customShortcuts, - customActions: configurations.customActions, - customLinkPrefixes: configurations.customLinkPrefixes, - enableUnfocusOnTapOutside: configurations.isOnTapOutsideEnabled, - dialogTheme: configurations.dialogTheme, - contentInsertionConfiguration: - configurations.contentInsertionConfiguration, ), ); - final editor = I18n( - initialLocale: context.quillSharedConfigurations?.locale, - child: selectionEnabled - ? _selectionGestureDetectorBuilder.build( - behavior: HitTestBehavior.translucent, - detectWordBoundary: configurations.detectWordBoundary, - child: child, - ) - : child, - ); + final editor = selectionEnabled + ? _selectionGestureDetectorBuilder.build( + behavior: HitTestBehavior.translucent, + detectWordBoundary: configurations.detectWordBoundary, + child: child, + ) + : child; if (isWeb()) { // Intercept RawKeyEvent on Web to prevent it from propagating to parents @@ -407,8 +414,11 @@ class _QuillEditorSelectionGestureDetectorBuilder return; } - final _platform = Theme.of(_state.context).platform; - if (isAppleOS(_platform)) { + final platform = Theme.of(_state.context).platform; + if (isAppleOS( + platform: platform, + supportWeb: true, + )) { renderEditor!.selectPositionAt( from: details.globalPosition, cause: SelectionChangedCause.longPress, @@ -427,17 +437,17 @@ class _QuillEditorSelectionGestureDetectorBuilder return false; } final pos = renderEditor!.getPositionForOffset(details.globalPosition); - final result = - editor!.widget.controller.document.querySegmentLeafNode(pos.offset); + final result = editor!.widget.configurations.controller.document + .querySegmentLeafNode(pos.offset); final line = result.line; if (line == null) { return false; } final segmentLeaf = result.leaf; if (segmentLeaf == null && line.length == 1) { - editor!.widget.controller.updateSelection( + editor!.widget.configurations.controller.updateSelection( TextSelection.collapsed(offset: pos.offset), - ChangeSource.LOCAL, + ChangeSource.local, ); return true; } @@ -480,8 +490,9 @@ class _QuillEditorSelectionGestureDetectorBuilder try { if (delegate.selectionEnabled && !_isPositionSelected(details)) { - final _platform = Theme.of(_state.context).platform; - if (isAppleOS(_platform) || isDesktop()) { + final platform = Theme.of(_state.context).platform; + if (isAppleOS(platform: platform, supportWeb: true) || + isDesktop(platform: platform, supportWeb: true)) { // added isDesktop() to enable extend selection in Windows platform switch (details.kind) { case PointerDeviceKind.mouse: @@ -544,8 +555,11 @@ class _QuillEditorSelectionGestureDetectorBuilder } if (delegate.selectionEnabled) { - final _platform = Theme.of(_state.context).platform; - if (isAppleOS(_platform)) { + final platform = Theme.of(_state.context).platform; + if (isAppleOS( + platform: platform, + supportWeb: true, + )) { renderEditor!.selectPositionAt( from: details.globalPosition, cause: SelectionChangedCause.longPress, @@ -613,20 +627,20 @@ class RenderEditor extends RenderEditableContainerBox implements RenderAbstractEditor { RenderEditor({ required this.document, - required TextDirection textDirection, + required super.textDirection, required bool hasFocus, required this.selection, required this.scrollable, required LayerLink startHandleLayerLink, required LayerLink endHandleLayerLink, - required EdgeInsetsGeometry padding, + required super.padding, required CursorCont cursorController, required this.onSelectionChanged, required this.onSelectionCompleted, - required double scrollBottomInset, + required super.scrollBottomInset, required this.floatingCursorDisabled, ViewportOffset? offset, - List? children, + super.children, EdgeInsets floatingCursorAddedMargin = const EdgeInsets.fromLTRB(4, 4, 4, 5), double? maxContentWidth, @@ -637,11 +651,7 @@ class RenderEditor extends RenderEditableContainerBox _cursorController = cursorController, _maxContentWidth = maxContentWidth, super( - children: children, container: document.root, - textDirection: textDirection, - scrollBottomInset: scrollBottomInset, - padding: padding, ); final CursorCont _cursorController; @@ -1495,7 +1505,7 @@ class RenderEditor extends RenderEditableContainerBox } } -class QuillVerticalCaretMovementRun extends Iterator { +class QuillVerticalCaretMovementRun implements Iterator { QuillVerticalCaretMovementRun._( this._editor, this._currentTextPosition, diff --git a/lib/src/widgets/editor/editor_builder.dart b/lib/src/widgets/editor/editor_builder.dart new file mode 100644 index 00000000..926af254 --- /dev/null +++ b/lib/src/widgets/editor/editor_builder.dart @@ -0,0 +1,31 @@ +import 'package:flutter/material.dart'; + +import '../raw_editor/raw_editor.dart'; + +typedef QuillEditorBuilder = Widget Function( + BuildContext context, + QuillRawEditor rawEditor, +); + +class QuillEditorBuilderWidget extends StatelessWidget { + const QuillEditorBuilderWidget({ + required this.child, + this.builder, + super.key, + }); + + final QuillRawEditor child; + final QuillEditorBuilder? builder; + + @override + Widget build(BuildContext context) { + final builderCallback = builder; + if (builderCallback != null) { + return builderCallback( + context, + child, + ); + } + return child; + } +} diff --git a/lib/src/widgets/embeds.dart b/lib/src/widgets/embeds.dart index 11d34b87..87511204 100644 --- a/lib/src/widgets/embeds.dart +++ b/lib/src/widgets/embeds.dart @@ -29,7 +29,8 @@ abstract class EmbedBuilder { } typedef EmbedButtonBuilder = Widget Function( - QuillController controller, - double toolbarIconSize, - QuillIconTheme? iconTheme, - QuillDialogTheme? dialogTheme); + QuillController controller, + double toolbarIconSize, + QuillIconTheme? iconTheme, + QuillDialogTheme? dialogTheme, +); diff --git a/lib/src/widgets/keyboard_listener.dart b/lib/src/widgets/keyboard_listener.dart index 47cb9555..cba72b4f 100644 --- a/lib/src/widgets/keyboard_listener.dart +++ b/lib/src/widgets/keyboard_listener.dart @@ -31,8 +31,7 @@ class QuillPressedKeys extends ChangeNotifier { } class QuillKeyboardListener extends StatefulWidget { - const QuillKeyboardListener({required this.child, Key? key}) - : super(key: key); + const QuillKeyboardListener({required this.child, super.key}); final Widget child; @@ -44,8 +43,9 @@ class QuillKeyboardListenerState extends State { final QuillPressedKeys _pressedKeys = QuillPressedKeys(); bool _keyEvent(KeyEvent event) { - _pressedKeys - ._updatePressedKeys(HardwareKeyboard.instance.logicalKeysPressed); + _pressedKeys._updatePressedKeys( + HardwareKeyboard.instance.logicalKeysPressed, + ); return false; } @@ -76,9 +76,8 @@ class QuillKeyboardListenerState extends State { class _QuillPressedKeysAccess extends InheritedWidget { const _QuillPressedKeysAccess({ required this.pressedKeys, - required Widget child, - Key? key, - }) : super(key: key, child: child); + required super.child, + }); final QuillPressedKeys pressedKeys; diff --git a/lib/src/widgets/link.dart b/lib/src/widgets/link.dart index 345a0027..9e4ee354 100644 --- a/lib/src/widgets/link.dart +++ b/lib/src/widgets/link.dart @@ -2,9 +2,9 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import '../l10n/extensions/localizations.dart'; import '../models/documents/attribute.dart'; import '../models/documents/nodes/node.dart'; -import '../translations/toolbar.i18n.dart'; const linkPrefixes = [ 'mailto:', // email @@ -124,8 +124,7 @@ class _CupertinoAction extends StatelessWidget { required this.title, required this.icon, required this.onPressed, - Key? key, - }) : super(key: key); + }); final String title; final IconData icon; @@ -168,17 +167,17 @@ Future _showMaterialMenu( mainAxisSize: MainAxisSize.min, children: [ _MaterialAction( - title: 'Open'.i18n, + title: context.loc.open, icon: Icons.language_sharp, onPressed: () => Navigator.of(context).pop(LinkMenuAction.launch), ), _MaterialAction( - title: 'Copy'.i18n, + title: context.loc.copy, icon: Icons.copy_sharp, onPressed: () => Navigator.of(context).pop(LinkMenuAction.copy), ), _MaterialAction( - title: 'Remove'.i18n, + title: context.loc.remove, icon: Icons.link_off_sharp, onPressed: () => Navigator.of(context).pop(LinkMenuAction.remove), ), @@ -195,8 +194,7 @@ class _MaterialAction extends StatelessWidget { required this.title, required this.icon, required this.onPressed, - Key? key, - }) : super(key: key); + }); final String title; final IconData icon; diff --git a/lib/src/widgets/proxy.dart b/lib/src/widgets/proxy.dart index 155e620a..e6475fdd 100644 --- a/lib/src/widgets/proxy.dart +++ b/lib/src/widgets/proxy.dart @@ -6,8 +6,12 @@ import 'package:flutter/widgets.dart'; import 'box.dart'; class BaselineProxy extends SingleChildRenderObjectWidget { - const BaselineProxy({Key? key, Widget? child, this.textStyle, this.padding}) - : super(key: key, child: child); + const BaselineProxy({ + super.key, + super.child, + this.textStyle, + this.padding, + }); final TextStyle? textStyle; final EdgeInsets? padding; @@ -32,15 +36,14 @@ class BaselineProxy extends SingleChildRenderObjectWidget { class RenderBaselineProxy extends RenderProxyBox { RenderBaselineProxy( - RenderParagraph? child, + RenderParagraph? super.child, TextStyle textStyle, EdgeInsets? padding, - ) : _prototypePainter = TextPainter( + ) : _prototypePainter = TextPainter( text: TextSpan(text: ' ', style: textStyle), textDirection: TextDirection.ltr, strutStyle: - StrutStyle.fromTextStyle(textStyle, forceStrutHeight: true)), - super(child); + StrutStyle.fromTextStyle(textStyle, forceStrutHeight: true)); final TextPainter _prototypePainter; @@ -75,7 +78,7 @@ class RenderBaselineProxy extends RenderProxyBox { } class EmbedProxy extends SingleChildRenderObjectWidget { - const EmbedProxy(Widget child) : super(child: child); + const EmbedProxy(Widget child, {super.key}) : super(child: child); @override RenderEmbedProxy createRenderObject(BuildContext context) => @@ -83,7 +86,7 @@ class EmbedProxy extends SingleChildRenderObjectWidget { } class RenderEmbedProxy extends RenderProxyBox implements RenderContentProxyBox { - RenderEmbedProxy(RenderBox? child) : super(child); + RenderEmbedProxy(super.child); @override List getBoxesForSelection(TextSelection selection) { @@ -127,7 +130,7 @@ class RenderEmbedProxy extends RenderProxyBox implements RenderContentProxyBox { class RichTextProxy extends SingleChildRenderObjectWidget { /// Child argument should be an instance of RichText widget. const RichTextProxy( - {required RichText child, + {required RichText super.child, required this.textStyle, required this.textAlign, required this.textDirection, @@ -136,8 +139,7 @@ class RichTextProxy extends SingleChildRenderObjectWidget { this.textScaleFactor = 1.0, this.textWidthBasis = TextWidthBasis.parent, this.textHeightBehavior, - Key? key}) - : super(key: key, child: child); + super.key}); final TextStyle textStyle; final TextAlign textAlign; @@ -180,7 +182,7 @@ class RichTextProxy extends SingleChildRenderObjectWidget { class RenderParagraphProxy extends RenderProxyBox implements RenderContentProxyBox { RenderParagraphProxy( - RenderParagraph? child, + RenderParagraph? super.child, TextStyle textStyle, TextAlign textAlign, TextDirection textDirection, @@ -189,7 +191,7 @@ class RenderParagraphProxy extends RenderProxyBox Locale locale, TextWidthBasis textWidthBasis, TextHeightBehavior? textHeightBehavior, - ) : _prototypePainter = TextPainter( + ) : _prototypePainter = TextPainter( text: TextSpan(text: ' ', style: textStyle), textAlign: textAlign, textDirection: textDirection, @@ -197,8 +199,7 @@ class RenderParagraphProxy extends RenderProxyBox strutStyle: strutStyle, locale: locale, textWidthBasis: textWidthBasis, - textHeightBehavior: textHeightBehavior), - super(child); + textHeightBehavior: textHeightBehavior); final TextPainter _prototypePainter; diff --git a/lib/src/widgets/quill_single_child_scroll_view.dart b/lib/src/widgets/quill_single_child_scroll_view.dart index b3efadec..4a60a386 100644 --- a/lib/src/widgets/quill_single_child_scroll_view.dart +++ b/lib/src/widgets/quill_single_child_scroll_view.dart @@ -14,10 +14,10 @@ class QuillSingleChildScrollView extends StatelessWidget { const QuillSingleChildScrollView({ required this.controller, required this.viewportBuilder, - Key? key, + super.key, this.physics, this.restorationId, - }) : super(key: key); + }); /// An object that can be used to control the position to which this scroll /// view is scrolled. @@ -48,7 +48,10 @@ class QuillSingleChildScrollView extends StatelessWidget { AxisDirection _getDirection(BuildContext context) { return getAxisDirectionFromAxisReverseAndDirectionality( - context, Axis.vertical, false); + context, + Axis.vertical, + false, + ); } @override @@ -74,9 +77,8 @@ class QuillSingleChildScrollView extends StatelessWidget { class _SingleChildViewport extends SingleChildRenderObjectWidget { const _SingleChildViewport({ required this.offset, - Key? key, - Widget? child, - }) : super(key: key, child: child); + super.child, + }); final ViewportOffset offset; diff --git a/lib/src/widgets/raw_editor/raw_editor.dart b/lib/src/widgets/raw_editor/raw_editor.dart index 2cd2b767..dc13082f 100644 --- a/lib/src/widgets/raw_editor/raw_editor.dart +++ b/lib/src/widgets/raw_editor/raw_editor.dart @@ -6,18 +6,15 @@ import 'dart:ui' as ui hide TextStyle; import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart' show defaultTargetPlatform; import 'package:flutter/material.dart'; -import 'package:flutter/rendering.dart' - show RenderAbstractViewport, ViewportOffset; +import 'package:flutter/rendering.dart' show RenderAbstractViewport; import 'package:flutter/scheduler.dart' show SchedulerBinding; import 'package:flutter/services.dart' show LogicalKeyboardKey, - Uint8List, RawKeyDownEvent, HardwareKeyboard, Clipboard, ClipboardData, - TextLayoutMetrics, TextInputControl; import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart' show KeyboardVisibilityController; @@ -32,16 +29,13 @@ import '../../models/documents/nodes/line.dart'; import '../../models/documents/nodes/node.dart'; import '../../models/structs/offset_value.dart'; import '../../models/structs/vertical_spacing.dart'; -import '../../models/themes/quill_dialog_theme.dart'; import '../../utils/cast.dart'; import '../../utils/delta.dart'; import '../../utils/embeds.dart'; -import '../../utils/extensions/build_context.dart'; import '../../utils/platform.dart'; import '../controller.dart'; import '../cursor.dart'; import '../default_styles.dart'; -import '../delegate.dart'; import '../editor/editor.dart'; import '../keyboard_listener.dart'; import '../link.dart'; @@ -50,251 +44,18 @@ import '../quill_single_child_scroll_view.dart'; import '../text_block.dart'; import '../text_line.dart'; import '../text_selection.dart'; -import '../toolbar/buttons/link_style2.dart'; -import '../toolbar/buttons/search/search_dialog.dart'; +import 'raw_editor.dart'; +import 'raw_editor_actions.dart'; +import 'raw_editor_render_object.dart'; import 'raw_editor_state_selection_delegate_mixin.dart'; import 'raw_editor_state_text_input_client_mixin.dart'; +import 'raw_editor_text_boundaries.dart'; -class RawEditor extends StatefulWidget { - const RawEditor({ - required this.controller, - required this.focusNode, - required this.scrollController, - required this.scrollBottomInset, - required this.cursorStyle, - required this.selectionColor, - required this.selectionCtrls, - required this.embedBuilder, - required this.autoFocus, - super.key, - this.scrollable = true, - this.padding = EdgeInsets.zero, - this.readOnly = false, - this.placeholder, - this.onLaunchUrl, - this.contextMenuBuilder = defaultContextMenuBuilder, - this.showSelectionHandles = false, - bool? showCursor, - this.textCapitalization = TextCapitalization.none, - this.maxHeight, - this.minHeight, - this.maxContentWidth, - this.customStyles, - this.customShortcuts, - this.customActions, - this.expands = false, - this.enableUnfocusOnTapOutside = true, - this.keyboardAppearance = Brightness.light, - this.enableInteractiveSelection = true, - this.scrollPhysics, - this.linkActionPickerDelegate = defaultLinkActionPickerDelegate, - this.customStyleBuilder, - this.customRecognizerBuilder, - this.floatingCursorDisabled = false, - this.onImagePaste, - this.customLinkPrefixes = const [], - this.dialogTheme, - this.contentInsertionConfiguration, - }) : assert(maxHeight == null || maxHeight > 0, 'maxHeight cannot be null'), - assert(minHeight == null || minHeight >= 0, 'minHeight cannot be null'), - assert(maxHeight == null || minHeight == null || maxHeight >= minHeight, - 'maxHeight cannot be null'), - showCursor = showCursor ?? true; - - /// Controls the document being edited. - final QuillController controller; - - /// Controls whether this editor has keyboard focus. - final FocusNode focusNode; - final ScrollController scrollController; - final bool scrollable; - final double scrollBottomInset; - final bool enableUnfocusOnTapOutside; - - /// Additional space around the editor contents. - final EdgeInsetsGeometry padding; - - /// Whether the text can be changed. - /// - /// When this is set to true, the text cannot be modified - /// by any shortcut or keyboard operation. The text is still selectable. - /// - /// Defaults to false. Must not be null. - final bool readOnly; - - final String? placeholder; - - /// Callback which is triggered when the user wants to open a URL from - /// a link in the document. - final ValueChanged? onLaunchUrl; - - /// Builds the text selection toolbar when requested by the user. - /// - /// See also: - /// * [EditableText.contextMenuBuilder], which builds the default - /// text selection toolbar for [EditableText]. - /// - /// If not provided, no context menu will be shown. - final QuillEditorContextMenuBuilder? contextMenuBuilder; - - static Widget defaultContextMenuBuilder( - BuildContext context, - RawEditorState state, - ) { - return TextFieldTapRegion( - child: AdaptiveTextSelectionToolbar.buttonItems( - buttonItems: state.contextMenuButtonItems, - anchors: state.contextMenuAnchors, - ), - ); - } - - /// Whether to show selection handles. - /// - /// When a selection is active, there will be two handles at each side of - /// boundary, or one handle if the selection is collapsed. The handles can be - /// dragged to adjust the selection. - /// - /// See also: - /// - /// * [showCursor], which controls the visibility of the cursor. - final bool showSelectionHandles; - - /// Whether to show cursor. - /// - /// The cursor refers to the blinking caret when the editor is focused. - /// - /// See also: - /// - /// * [cursorStyle], which controls the cursor visual representation. - /// * [showSelectionHandles], which controls the visibility of the selection - /// handles. - final bool showCursor; - - /// The style to be used for the editing cursor. - final CursorStyle cursorStyle; - - /// Configures how the platform keyboard will select an uppercase or - /// lowercase keyboard. - /// - /// Only supports text keyboards, other keyboard types will ignore this - /// configuration. Capitalization is locale-aware. - /// - /// Defaults to [TextCapitalization.none]. Must not be null. - /// - /// See also: - /// - /// * [TextCapitalization], for a description of each capitalization behavior - final TextCapitalization textCapitalization; - - /// The maximum height this editor can have. - /// - /// If this is null then there is no limit to the editor's height and it will - /// expand to fill its parent. - final double? maxHeight; - - /// The minimum height this editor can have. - final double? minHeight; - - /// The maximum width to be occupied by the content of this editor. - /// - /// If this is not null and and this editor's width is larger than this value - /// then the contents will be constrained to the provided maximum width and - /// horizontally centered. This is mostly useful on devices with wide screens. - final double? maxContentWidth; - - /// Allows to override [DefaultStyles]. - final DefaultStyles? customStyles; - - /// Whether this widget's height will be sized to fill its parent. - /// - /// If set to true and wrapped in a parent widget like [Expanded] or - /// - /// Defaults to false. - final bool expands; - - /// Whether this editor should focus itself if nothing else is already - /// focused. - /// - /// If true, the keyboard will open as soon as this text field obtains focus. - /// Otherwise, the keyboard is only shown after the user taps the text field. - /// - /// Defaults to false. Cannot be null. - final bool autoFocus; - - /// The color to use when painting the selection. - final Color selectionColor; - - /// Delegate for building the text selection handles and toolbar. - /// - /// The [RawEditor] widget used on its own will not trigger the display - /// of the selection toolbar by itself. The toolbar is shown by calling - /// [RawEditorState.showToolbar] in response to an appropriate user event. - final TextSelectionControls selectionCtrls; - - /// The appearance of the keyboard. - /// - /// This setting is only honored on iOS devices. - /// - /// Defaults to [Brightness.light]. - final Brightness keyboardAppearance; - - /// If true, then long-pressing this TextField will select text and show the - /// cut/copy/paste menu, and tapping will move the text caret. - /// - /// True by default. - /// - /// If false, most of the accessibility support for selecting text, copy - /// and paste, and moving the caret will be disabled. - final bool enableInteractiveSelection; - - bool get selectionEnabled => enableInteractiveSelection; - - /// The [ScrollPhysics] to use when vertically scrolling the input. - /// - /// If not specified, it will behave according to the current platform. - /// - /// See [Scrollable.physics]. - final ScrollPhysics? scrollPhysics; - - final Future Function(Uint8List imageBytes)? onImagePaste; - - /// Contains user-defined shortcuts map. - /// - /// [https://docs.flutter.dev/development/ui/advanced/actions-and-shortcuts#shortcuts] - final Map? customShortcuts; - - /// Contains user-defined actions. - /// - /// [https://docs.flutter.dev/development/ui/advanced/actions-and-shortcuts#actions] - final Map>? customActions; - - /// Builder function for embeddable objects. - final EmbedsBuilder embedBuilder; - final LinkActionPickerDelegate linkActionPickerDelegate; - final CustomStyleBuilder? customStyleBuilder; - final CustomRecognizerBuilder? customRecognizerBuilder; - final bool floatingCursorDisabled; - final List customLinkPrefixes; - - /// Configures the dialog theme. - final QuillDialogTheme? dialogTheme; - - /// Configuration of handler for media content inserted via the system input - /// method. - /// - /// See [https://api.flutter.dev/flutter/widgets/EditableText/contentInsertionConfiguration.html] - final ContentInsertionConfiguration? contentInsertionConfiguration; - - @override - State createState() => RawEditorState(); -} - -class RawEditorState extends EditorState +class QuillRawEditorState extends EditorState with - AutomaticKeepAliveClientMixin, + AutomaticKeepAliveClientMixin, WidgetsBindingObserver, - TickerProviderStateMixin, + TickerProviderStateMixin, RawEditorStateTextInputClientMixin, RawEditorStateSelectionDelegateMixin { final GlobalKey _editorKey = GlobalKey(); @@ -315,12 +76,12 @@ class RawEditorState extends EditorState // Cursors late CursorCont _cursorCont; - QuillController get controller => widget.controller; + QuillController get controller => widget.configurations.controller; // Focus bool _didAutoFocus = false; - bool get _hasFocus => widget.focusNode.hasFocus; + bool get _hasFocus => widget.configurations.focusNode.hasFocus; // Theme DefaultStyles? _styles; @@ -347,23 +108,21 @@ class RawEditorState extends EditorState @override void insertContent(KeyboardInsertedContent content) { - assert(widget.contentInsertionConfiguration?.allowedMimeTypes + assert(widget.configurations.contentInsertionConfiguration?.allowedMimeTypes .contains(content.mimeType) ?? false); - widget.contentInsertionConfiguration?.onContentInserted.call(content); + widget.configurations.contentInsertionConfiguration?.onContentInserted + .call(content); } /// Returns the [ContextMenuButtonItem]s representing the buttons in this - /// platform's default selection menu for [RawEditor]. + /// platform's default selection menu for [QuillRawEditor]. /// /// Copied from [EditableTextState]. List get contextMenuButtonItems { return EditableText.getEditableButtonItems( clipboardStatus: _clipboardStatus.value, onLiveTextInput: null, - onLookUp: null, - onSearchWeb: null, - onShare: null, onCopy: copyEnabled ? () => copySelection(SelectionChangedCause.toolbar) : null, @@ -374,6 +133,9 @@ class RawEditorState extends EditorState onSelectAll: selectAllEnabled ? () => selectAll(SelectionChangedCause.toolbar) : null, + onLookUp: null, + onSearchWeb: null, + onShare: null, ); } @@ -393,10 +155,10 @@ class RawEditorState extends EditorState } /// Gets the line heights at the start and end of the selection for the given - /// [RawEditorState]. + /// [QuillRawEditorState]. /// /// Copied from [EditableTextState]. - _GlyphHeights _getGlyphHeights() { + QuillEditorGlyphHeights _getGlyphHeights() { final selection = textEditingValue.selection; // Only calculate handle rects if the text in the previous frame @@ -409,7 +171,7 @@ class RawEditorState extends EditorState final prevText = renderEditor.document.toPlainText(); final currText = textEditingValue.text; if (prevText != currText || !selection.isValid || selection.isCollapsed) { - return _GlyphHeights( + return QuillEditorGlyphHeights( renderEditor.preferredLineHeight(selection.base), renderEditor.preferredLineHeight(selection.base), ); @@ -419,17 +181,13 @@ class RawEditorState extends EditorState renderEditor.getLocalRectForCaret(selection.base); final endCharacterRect = renderEditor.getLocalRectForCaret(selection.extent); - return _GlyphHeights( + return QuillEditorGlyphHeights( startCharacterRect.height, endCharacterRect.height, ); } void _defaultOnTapOutside(PointerDownEvent event) { - if (isWeb()) { - widget.focusNode.unfocus(); - } - /// The focus dropping behavior is only present on desktop platforms /// and mobile browsers. switch (defaultTargetPlatform) { @@ -440,31 +198,28 @@ class RawEditorState extends EditorState // in the web browser, but we do unfocus for all other kinds of events. switch (event.kind) { case ui.PointerDeviceKind.touch: - // if (isWeb()) { - // widget.focusNode.unfocus(); - // } break; case ui.PointerDeviceKind.mouse: case ui.PointerDeviceKind.stylus: case ui.PointerDeviceKind.invertedStylus: case ui.PointerDeviceKind.unknown: - widget.focusNode.unfocus(); + widget.configurations.focusNode.unfocus(); break; case ui.PointerDeviceKind.trackpad: throw UnimplementedError( - 'Unexpected pointer down event for trackpad', + 'Unexpected pointer down event for trackpad.', ); } break; case TargetPlatform.linux: case TargetPlatform.macOS: case TargetPlatform.windows: - widget.focusNode.unfocus(); + widget.configurations.focusNode.unfocus(); break; default: throw UnsupportedError( 'The platform ${defaultTargetPlatform.name} is not supported in the' - ' _defaultOnTapOutside', + ' _defaultOnTapOutside()', ); } } @@ -474,11 +229,14 @@ class RawEditorState extends EditorState assert(debugCheckHasMediaQuery(context)); super.build(context); - var _doc = controller.document; - if (_doc.isEmpty() && widget.placeholder != null) { - final raw = widget.placeholder?.replaceAll(r'"', '\\"'); - _doc = Document.fromJson(jsonDecode( - '[{"attributes":{"placeholder":true},"insert":"$raw\\n"}]')); + var doc = controller.document; + if (doc.isEmpty() && widget.configurations.placeholder != null) { + final raw = widget.configurations.placeholder?.replaceAll(r'"', '\\"'); + doc = Document.fromJson( + jsonDecode( + '[{"attributes":{"placeholder":true},"insert":"$raw\\n"}]', + ), + ); } Widget child = CompositedTransformTarget( @@ -486,29 +244,30 @@ class RawEditorState extends EditorState child: Semantics( child: MouseRegion( cursor: SystemMouseCursors.text, - child: _Editor( + child: QuilRawEditorMultiChildRenderObject( key: _editorKey, - document: _doc, + document: doc, selection: controller.selection, hasFocus: _hasFocus, - scrollable: widget.scrollable, + scrollable: widget.configurations.scrollable, cursorController: _cursorCont, textDirection: _textDirection, startHandleLayerLink: _startHandleLayerLink, endHandleLayerLink: _endHandleLayerLink, onSelectionChanged: _handleSelectionChanged, onSelectionCompleted: _handleSelectionCompleted, - scrollBottomInset: widget.scrollBottomInset, - padding: widget.padding, - maxContentWidth: widget.maxContentWidth, - floatingCursorDisabled: widget.floatingCursorDisabled, - children: _buildChildren(_doc, context), + scrollBottomInset: widget.configurations.scrollBottomInset, + padding: widget.configurations.padding, + maxContentWidth: widget.configurations.maxContentWidth, + floatingCursorDisabled: + widget.configurations.floatingCursorDisabled, + children: _buildChildren(doc, context), ), ), ), ); - if (widget.scrollable) { + if (widget.configurations.scrollable) { /// Since [SingleChildScrollView] does not implement /// `computeDistanceToActualBaseline` it prevents the editor from /// providing its baseline metrics. To address this issue we wrap @@ -522,29 +281,30 @@ class RawEditorState extends EditorState padding: baselinePadding, child: QuillSingleChildScrollView( controller: _scrollController, - physics: widget.scrollPhysics, + physics: widget.configurations.scrollPhysics, viewportBuilder: (_, offset) => CompositedTransformTarget( link: _toolbarLayerLink, child: MouseRegion( cursor: SystemMouseCursors.text, - child: _Editor( + child: QuilRawEditorMultiChildRenderObject( key: _editorKey, offset: offset, - document: _doc, + document: doc, selection: controller.selection, hasFocus: _hasFocus, - scrollable: widget.scrollable, + scrollable: widget.configurations.scrollable, textDirection: _textDirection, startHandleLayerLink: _startHandleLayerLink, endHandleLayerLink: _endHandleLayerLink, onSelectionChanged: _handleSelectionChanged, onSelectionCompleted: _handleSelectionCompleted, - scrollBottomInset: widget.scrollBottomInset, - padding: widget.padding, - maxContentWidth: widget.maxContentWidth, + scrollBottomInset: widget.configurations.scrollBottomInset, + padding: widget.configurations.padding, + maxContentWidth: widget.configurations.maxContentWidth, cursorController: _cursorCont, - floatingCursorDisabled: widget.floatingCursorDisabled, - children: _buildChildren(_doc, context), + floatingCursorDisabled: + widget.configurations.floatingCursorDisabled, + children: _buildChildren(doc, context), ), ), ), @@ -552,27 +312,25 @@ class RawEditorState extends EditorState ); } - final constraints = widget.expands + final constraints = widget.configurations.expands ? const BoxConstraints.expand() : BoxConstraints( - minHeight: widget.minHeight ?? 0.0, - maxHeight: widget.maxHeight ?? double.infinity, + minHeight: widget.configurations.minHeight ?? 0.0, + maxHeight: widget.configurations.maxHeight ?? double.infinity, ); // Please notice that this change will make the check fixed // so if we ovveride the platform in material app theme data // it will not depend on it and doesn't change here but I don't think // we need to - final isDesktopMacOS = isMacOS(); + final isDesktopMacOS = isMacOS(supportWeb: true); return TextFieldTapRegion( - enabled: widget.enableUnfocusOnTapOutside, + enabled: widget.configurations.isOnTapOutsideEnabled, onTapOutside: (event) { - final onTapOutside = - context.requireQuillEditorConfigurations.onTapOutside; + final onTapOutside = widget.configurations.onTapOutside; if (onTapOutside != null) { - context.requireQuillEditorConfigurations.onTapOutside - ?.call(event, widget.focusNode); + onTapOutside.call(event, widget.configurations.focusNode); return; } _defaultOnTapOutside(event); @@ -639,7 +397,7 @@ class RawEditorState extends EditorState LogicalKeyboardKey.keyK, control: !isDesktopMacOS, meta: isDesktopMacOS, - ): const ApplyLinkIntent(), + ): const QuillEditorApplyLinkIntent(), // Lists SingleActivator( @@ -659,7 +417,7 @@ class RawEditorState extends EditorState control: !isDesktopMacOS, meta: isDesktopMacOS, shift: true, - ): const ApplyCheckListIntent(), + ): const QuillEditorApplyCheckListIntent(), // Indents SingleActivator( @@ -679,28 +437,28 @@ class RawEditorState extends EditorState LogicalKeyboardKey.digit1, control: !isDesktopMacOS, meta: isDesktopMacOS, - ): const ApplyHeaderIntent(Attribute.h1), + ): const QuillEditorApplyHeaderIntent(Attribute.h1), SingleActivator( LogicalKeyboardKey.digit2, control: !isDesktopMacOS, meta: isDesktopMacOS, - ): const ApplyHeaderIntent(Attribute.h2), + ): const QuillEditorApplyHeaderIntent(Attribute.h2), SingleActivator( LogicalKeyboardKey.digit3, control: !isDesktopMacOS, meta: isDesktopMacOS, - ): const ApplyHeaderIntent(Attribute.h3), + ): const QuillEditorApplyHeaderIntent(Attribute.h3), SingleActivator( LogicalKeyboardKey.digit0, control: !isDesktopMacOS, meta: isDesktopMacOS, - ): const ApplyHeaderIntent(Attribute.header), + ): const QuillEditorApplyHeaderIntent(Attribute.header), SingleActivator( LogicalKeyboardKey.keyG, control: !isDesktopMacOS, meta: isDesktopMacOS, - ): const InsertEmbedIntent(Attribute.image), + ): const QuillEditorInsertEmbedIntent(Attribute.image), SingleActivator( LogicalKeyboardKey.keyF, @@ -708,14 +466,14 @@ class RawEditorState extends EditorState meta: isDesktopMacOS, ): const OpenSearchIntent(), }, { - ...?widget.customShortcuts + ...?widget.configurations.customShortcuts }), child: Actions( actions: mergeMaps>(_actions, { - ...?widget.customActions, + ...?widget.configurations.customActions, }), child: Focus( - focusNode: widget.focusNode, + focusNode: widget.configurations.focusNode, onKey: _onKey, child: QuillKeyboardListener( child: Container( @@ -794,7 +552,7 @@ class RawEditorState extends EditorState controller.document.queryChild(controller.selection.baseOffset); KeyEventResult insertTabCharacter() { - if (widget.readOnly) { + if (widget.configurations.isReadOnly) { return KeyEventResult.ignored; } controller.replaceText(controller.selection.baseOffset, 0, '\t', null); @@ -856,7 +614,7 @@ class RawEditorState extends EditorState controller.selection.copyWith( baseOffset: selection.baseOffset + chars, extentOffset: selection.baseOffset + chars), - ChangeSource.LOCAL); + ChangeSource.local); } void _updateSelectionForKeyPhrase(String phrase, Attribute attribute) { @@ -874,7 +632,7 @@ class RawEditorState extends EditorState SelectionChangedCause cause, ) { final oldSelection = controller.selection; - controller.updateSelection(selection, ChangeSource.LOCAL); + controller.updateSelection(selection, ChangeSource.local); _selectionOverlay?.handlesVisible = _shouldShowSelectionHandles(); @@ -902,10 +660,9 @@ class RawEditorState extends EditorState /// Updates the checkbox positioned at [offset] in document /// by changing its attribute according to [value]. void _handleCheckboxTap(int offset, bool value) { - final requestKeyboardFocusOnCheckListChanged = context - .requireQuillEditorConfigurations - .requestKeyboardFocusOnCheckListChanged; - if (!widget.readOnly) { + final requestKeyboardFocusOnCheckListChanged = + widget.configurations.requestKeyboardFocusOnCheckListChanged; + if (!widget.configurations.isReadOnly) { _disableScrollControllerAnimateOnce = true; final currentSelection = controller.selection.copyWith(); final attribute = value ? Attribute.checked : Attribute.unchecked; @@ -928,7 +685,7 @@ class RawEditorState extends EditorState controller ..ignoreFocusOnTextChange = false ..skipRequestKeyboard = !requestKeyboardFocusOnCheckListChanged - ..updateSelection(currentSelection, ChangeSource.LOCAL); + ..updateSelection(currentSelection, ChangeSource.local); }); } } @@ -962,26 +719,27 @@ class RawEditorState extends EditorState block: node, controller: controller, textDirection: getDirectionOfNode(node), - scrollBottomInset: widget.scrollBottomInset, + scrollBottomInset: widget.configurations.scrollBottomInset, verticalSpacing: _getVerticalSpacingForBlock(node, _styles), textSelection: controller.selection, - color: widget.selectionColor, + color: widget.configurations.selectionColor, styles: _styles, - enableInteractiveSelection: widget.enableInteractiveSelection, + enableInteractiveSelection: + widget.configurations.enableInteractiveSelection, hasFocus: _hasFocus, contentPadding: attrs.containsKey(Attribute.codeBlock.key) ? const EdgeInsets.all(16) : null, - embedBuilder: widget.embedBuilder, + embedBuilder: widget.configurations.embedBuilder, linkActionPicker: _linkActionPicker, - onLaunchUrl: widget.onLaunchUrl, + onLaunchUrl: widget.configurations.onLaunchUrl, cursorCont: _cursorCont, indentLevelCounts: indentLevelCounts, clearIndents: clearIndents, onCheckboxTap: _handleCheckboxTap, - readOnly: widget.readOnly, - customStyleBuilder: widget.customStyleBuilder, - customLinkPrefixes: widget.customLinkPrefixes, + readOnly: widget.configurations.isReadOnly, + customStyleBuilder: widget.configurations.customStyleBuilder, + customLinkPrefixes: widget.configurations.customLinkPrefixes, ); result.add( Directionality( @@ -1005,15 +763,15 @@ class RawEditorState extends EditorState final textLine = TextLine( line: node, textDirection: _textDirection, - embedBuilder: widget.embedBuilder, - customStyleBuilder: widget.customStyleBuilder, - customRecognizerBuilder: widget.customRecognizerBuilder, + embedBuilder: widget.configurations.embedBuilder, + customStyleBuilder: widget.configurations.customStyleBuilder, + customRecognizerBuilder: widget.configurations.customRecognizerBuilder, styles: _styles!, - readOnly: widget.readOnly, + readOnly: widget.configurations.isReadOnly, controller: controller, linkActionPicker: _linkActionPicker, - onLaunchUrl: widget.onLaunchUrl, - customLinkPrefixes: widget.customLinkPrefixes, + onLaunchUrl: widget.configurations.onLaunchUrl, + customLinkPrefixes: widget.configurations.customLinkPrefixes, ); final editableTextLine = EditableTextLine( node, @@ -1023,8 +781,8 @@ class RawEditorState extends EditorState _getVerticalSpacingForLine(node, _styles), _textDirection, controller.selection, - widget.selectionColor, - widget.enableInteractiveSelection, + widget.configurations.selectionColor, + widget.configurations.enableInteractiveSelection, _hasFocus, MediaQuery.devicePixelRatioOf(context), _cursorCont); @@ -1032,7 +790,9 @@ class RawEditorState extends EditorState } VerticalSpacing _getVerticalSpacingForLine( - Line line, DefaultStyles? defaultStyles) { + Line line, + DefaultStyles? defaultStyles, + ) { final attrs = line.style.attributes; if (attrs.containsKey(Attribute.header.key)) { int level; @@ -1049,7 +809,7 @@ class RawEditorState extends EditorState case 3: return defaultStyles!.h3!.verticalSpacing; default: - throw 'Invalid level $level'; + throw ArgumentError('Invalid level $level'); } } @@ -1085,12 +845,12 @@ class RawEditorState extends EditorState controller.addListener(_didChangeTextEditingValueListener); - _scrollController = widget.scrollController; + _scrollController = widget.configurations.scrollController; _scrollController.addListener(_updateSelectionOverlayForScroll); _cursorCont = CursorCont( - show: ValueNotifier(widget.showCursor), - style: widget.cursorStyle, + show: ValueNotifier(widget.configurations.showCursor), + style: widget.configurations.cursorStyle, tickerProvider: this, ); @@ -1098,7 +858,7 @@ class RawEditorState extends EditorState _floatingCursorResetController = AnimationController(vsync: this); _floatingCursorResetController.addListener(onFloatingCursorResetTick); - if (isKeyboardOS()) { + if (isKeyboardOS(supportWeb: true)) { _keyboardVisible = true; } else if (!isWeb() && isFlutterTest()) { // treat tests like a keyboard OS @@ -1125,7 +885,7 @@ class RawEditorState extends EditorState } // Focus - widget.focusNode.addListener(_handleFocusChanged); + widget.configurations.focusNode.addListener(_handleFocusChanged); } // KeyboardVisibilityController only checks for keyboards that @@ -1158,47 +918,49 @@ class RawEditorState extends EditorState ? defaultStyles.merge(parentStyles) : defaultStyles; - if (widget.customStyles != null) { - _styles = _styles!.merge(widget.customStyles!); + if (widget.configurations.customStyles != null) { + _styles = _styles!.merge(widget.configurations.customStyles!); } _requestAutoFocusIfShould(); } Future _requestAutoFocusIfShould() async { - if (!_didAutoFocus && widget.autoFocus) { + final focusManager = FocusScope.of(context); + if (!_didAutoFocus && widget.configurations.autoFocus) { await Future.delayed(Duration.zero); // To avoid exceptions - FocusScope.of(context).autofocus(widget.focusNode); + focusManager.autofocus(widget.configurations.focusNode); _didAutoFocus = true; } } @override - void didUpdateWidget(RawEditor oldWidget) { + void didUpdateWidget(QuillRawEditor oldWidget) { super.didUpdateWidget(oldWidget); - _cursorCont.show.value = widget.showCursor; - _cursorCont.style = widget.cursorStyle; + _cursorCont.show.value = widget.configurations.showCursor; + _cursorCont.style = widget.configurations.cursorStyle; - if (controller != oldWidget.controller) { - oldWidget.controller.removeListener(_didChangeTextEditingValue); + if (controller != oldWidget.configurations.controller) { + oldWidget.configurations.controller + .removeListener(_didChangeTextEditingValue); controller.addListener(_didChangeTextEditingValue); updateRemoteValueIfNeeded(); } - if (widget.scrollController != _scrollController) { + if (widget.configurations.scrollController != _scrollController) { _scrollController.removeListener(_updateSelectionOverlayForScroll); - _scrollController = widget.scrollController; + _scrollController = widget.configurations.scrollController; _scrollController.addListener(_updateSelectionOverlayForScroll); } - if (widget.focusNode != oldWidget.focusNode) { - oldWidget.focusNode.removeListener(_handleFocusChanged); - widget.focusNode.addListener(_handleFocusChanged); + if (widget.configurations.focusNode != oldWidget.configurations.focusNode) { + oldWidget.configurations.focusNode.removeListener(_handleFocusChanged); + widget.configurations.focusNode.addListener(_handleFocusChanged); updateKeepAlive(); } - if (controller.selection != oldWidget.controller.selection) { + if (controller.selection != oldWidget.configurations.controller.selection) { _selectionOverlay?.update(textEditingValue); } @@ -1206,19 +968,20 @@ class RawEditorState extends EditorState if (!shouldCreateInputConnection) { closeConnectionIfNeeded(); } else { - if (oldWidget.readOnly && _hasFocus) { + if (oldWidget.configurations.isReadOnly && _hasFocus) { openConnectionIfNeeded(); } } // in case customStyles changed in new widget - if (widget.customStyles != null) { - _styles = _styles!.merge(widget.customStyles!); + if (widget.configurations.customStyles != null) { + _styles = _styles!.merge(widget.configurations.customStyles!); } } bool _shouldShowSelectionHandles() { - return widget.showSelectionHandles && !controller.selection.isCollapsed; + return widget.configurations.showSelectionHandles && + !controller.selection.isCollapsed; } @override @@ -1230,7 +993,7 @@ class RawEditorState extends EditorState _selectionOverlay?.dispose(); _selectionOverlay = null; controller.removeListener(_didChangeTextEditingValueListener); - widget.focusNode.removeListener(_handleFocusChanged); + widget.configurations.focusNode.removeListener(_handleFocusChanged); _cursorCont.dispose(); _clipboardStatus ..removeListener(_onChangedClipboardStatus) @@ -1330,12 +1093,13 @@ class RawEditorState extends EditorState startHandleLayerLink: _startHandleLayerLink, endHandleLayerLink: _endHandleLayerLink, renderObject: renderEditor, - selectionCtrls: widget.selectionCtrls, + selectionCtrls: widget.configurations.selectionCtrls, selectionDelegate: this, clipboardStatus: _clipboardStatus, - contextMenuBuilder: widget.contextMenuBuilder == null + contextMenuBuilder: widget.configurations.contextMenuBuilder == null ? null - : (context) => widget.contextMenuBuilder!(context, this), + : (context) => + widget.configurations.contextMenuBuilder!(context, this), ); _selectionOverlay!.handlesVisible = _shouldShowSelectionHandles(); _selectionOverlay!.showHandles(); @@ -1369,7 +1133,8 @@ class RawEditorState extends EditorState Future _linkActionPicker(Node linkNode) async { final link = linkNode.style.attributes[Attribute.link.key]!.value!; - return widget.linkActionPickerDelegate(context, link, linkNode); + return widget.configurations + .linkActionPickerDelegate(context, link, linkNode); } bool _showCaretOnScreenScheduled = false; @@ -1382,13 +1147,13 @@ class RawEditorState extends EditorState bool _disableScrollControllerAnimateOnce = false; void _showCaretOnScreen() { - if (!widget.showCursor || _showCaretOnScreenScheduled) { + if (!widget.configurations.showCursor || _showCaretOnScreenScheduled) { return; } _showCaretOnScreenScheduled = true; SchedulerBinding.instance.addPostFrameCallback((_) { - if (widget.scrollable || _scrollController.hasClients) { + if (widget.configurations.scrollable || _scrollController.hasClients) { _showCaretOnScreenScheduled = false; if (!mounted) { @@ -1438,7 +1203,6 @@ class RawEditorState extends EditorState @override void requestKeyboard() { if (controller.skipRequestKeyboard) { - // TODO: There is a bug, requestKeyboard is being called 2-4 times! // and that just by one simple change controller.skipRequestKeyboard = false; return; @@ -1456,7 +1220,7 @@ class RawEditorState extends EditorState _showCaretOnScreen(); } } else { - widget.focusNode.requestFocus(); + widget.configurations.focusNode.requestFocus(); } } @@ -1534,7 +1298,7 @@ class RawEditorState extends EditorState _pastePlainText = controller.getPlainText(); _pasteStyleAndEmbed = controller.getAllIndividualSelectionStylesAndEmbed(); - if (widget.readOnly) { + if (widget.configurations.isReadOnly) { return; } final selection = textEditingValue.selection; @@ -1554,7 +1318,7 @@ class RawEditorState extends EditorState /// Paste text from [Clipboard]. @override Future pasteText(SelectionChangedCause cause) async { - if (widget.readOnly) { + if (widget.configurations.isReadOnly) { return; } @@ -1615,7 +1379,7 @@ class RawEditorState extends EditorState return; } - final onImagePaste = widget.onImagePaste; + final onImagePaste = widget.configurations.onImagePaste; if (onImagePaste != null) { final image = await Pasteboard.image; @@ -1654,7 +1418,7 @@ class RawEditorState extends EditorState } @override - bool get wantKeepAlive => widget.focusNode.hasFocus; + bool get wantKeepAlive => widget.configurations.focusNode.hasFocus; @override AnimationController get floatingCursorResetController => @@ -1664,39 +1428,42 @@ class RawEditorState extends EditorState // --------------------------- Text Editing Actions -------------------------- - _TextBoundary _characterBoundary(DirectionalTextEditingIntent intent) { - final _TextBoundary atomicTextBoundary = - _CharacterBoundary(textEditingValue); - return _CollapsedSelectionBoundary(atomicTextBoundary, intent.forward); + QuillEditorTextBoundary _characterBoundary( + DirectionalTextEditingIntent intent) { + final atomicTextBoundary = QuillEditorCharacterBoundary(textEditingValue); + return QuillEditorCollapsedSelectionBoundary( + atomicTextBoundary, intent.forward); } - _TextBoundary _nextWordBoundary(DirectionalTextEditingIntent intent) { - final _TextBoundary atomicTextBoundary; - final _TextBoundary boundary; + QuillEditorTextBoundary _nextWordBoundary( + DirectionalTextEditingIntent intent) { + final QuillEditorTextBoundary atomicTextBoundary; + final QuillEditorTextBoundary boundary; // final TextEditingValue textEditingValue = // _textEditingValueforTextLayoutMetrics; - atomicTextBoundary = _CharacterBoundary(textEditingValue); + atomicTextBoundary = QuillEditorCharacterBoundary(textEditingValue); // This isn't enough. Newline characters. - boundary = _ExpandedTextBoundary(_WhitespaceBoundary(textEditingValue), - _WordBoundary(renderEditor, textEditingValue)); + boundary = QuillEditorExpandedTextBoundary( + QuillEditorWhitespaceBoundary(textEditingValue), + QuillEditorWordBoundary(renderEditor, textEditingValue)); final mixedBoundary = intent.forward - ? _MixedBoundary(atomicTextBoundary, boundary) - : _MixedBoundary(boundary, atomicTextBoundary); + ? QuillEditorMixedBoundary(atomicTextBoundary, boundary) + : QuillEditorMixedBoundary(boundary, atomicTextBoundary); // Use a _MixedBoundary to make sure we don't leave invalid codepoints in // the field after deletion. - return _CollapsedSelectionBoundary(mixedBoundary, intent.forward); + return QuillEditorCollapsedSelectionBoundary(mixedBoundary, intent.forward); } - _TextBoundary _linebreak(DirectionalTextEditingIntent intent) { - final _TextBoundary atomicTextBoundary; - final _TextBoundary boundary; + QuillEditorTextBoundary _linebreak(DirectionalTextEditingIntent intent) { + final QuillEditorTextBoundary atomicTextBoundary; + final QuillEditorTextBoundary boundary; // final TextEditingValue textEditingValue = // _textEditingValueforTextLayoutMetrics; - atomicTextBoundary = _CharacterBoundary(textEditingValue); - boundary = _LineBreak(renderEditor, textEditingValue); + atomicTextBoundary = QuillEditorCharacterBoundary(textEditingValue); + boundary = QuillEditorLineBreak(renderEditor, textEditingValue); // The _MixedBoundary is to make sure we don't leave invalid code units in // the field after deletion. @@ -1704,14 +1471,18 @@ class RawEditorState extends EditorState // since the document boundary is unique and the linebreak boundary is // already caret-location based. return intent.forward - ? _MixedBoundary( - _CollapsedSelectionBoundary(atomicTextBoundary, true), boundary) - : _MixedBoundary( - boundary, _CollapsedSelectionBoundary(atomicTextBoundary, false)); + ? QuillEditorMixedBoundary( + QuillEditorCollapsedSelectionBoundary(atomicTextBoundary, true), + boundary) + : QuillEditorMixedBoundary( + boundary, + QuillEditorCollapsedSelectionBoundary(atomicTextBoundary, false), + ); } - _TextBoundary _documentBoundary(DirectionalTextEditingIntent intent) => - _DocumentBoundary(textEditingValue); + QuillEditorTextBoundary _documentBoundary( + DirectionalTextEditingIntent intent) => + QuillEditorDocumentBoundary(textEditingValue); Action _makeOverridable(Action defaultAction) { return Action.overridable( @@ -1731,21 +1502,23 @@ class RawEditorState extends EditorState late final Action _updateSelectionAction = CallbackAction(onInvoke: _updateSelection); - late final _UpdateTextSelectionToAdjacentLineAction< + late final QuillEditorUpdateTextSelectionToAdjacentLineAction< ExtendSelectionVerticallyToAdjacentLineIntent> _adjacentLineAction = - _UpdateTextSelectionToAdjacentLineAction< + QuillEditorUpdateTextSelectionToAdjacentLineAction< ExtendSelectionVerticallyToAdjacentLineIntent>(this); - late final _ToggleTextStyleAction _formatSelectionAction = - _ToggleTextStyleAction(this); + late final QuillEditorToggleTextStyleAction _formatSelectionAction = + QuillEditorToggleTextStyleAction(this); - late final _IndentSelectionAction _indentSelectionAction = - _IndentSelectionAction(this); + late final QuillEditorIndentSelectionAction _indentSelectionAction = + QuillEditorIndentSelectionAction(this); - late final _OpenSearchAction _openSearchAction = _OpenSearchAction(this); - late final _ApplyHeaderAction _applyHeaderAction = _ApplyHeaderAction(this); - late final _ApplyCheckListAction _applyCheckListAction = - _ApplyCheckListAction(this); + late final QuillEditorOpenSearchAction _openSearchAction = + QuillEditorOpenSearchAction(this); + late final QuillEditorApplyHeaderAction _applyHeaderAction = + QuillEditorApplyHeaderAction(this); + late final QuillEditorApplyCheckListAction _applyCheckListAction = + QuillEditorApplyCheckListAction(this); late final Map> _actions = >{ DoNothingAndStopPropagationTextIntent: DoNothingAction(consumesKey: false), @@ -1755,53 +1528,58 @@ class RawEditorState extends EditorState // Delete DeleteCharacterIntent: _makeOverridable( - _DeleteTextAction(this, _characterBoundary)), + QuillEditorDeleteTextAction( + this, _characterBoundary)), DeleteToNextWordBoundaryIntent: _makeOverridable( - _DeleteTextAction( + QuillEditorDeleteTextAction( this, _nextWordBoundary)), DeleteToLineBreakIntent: _makeOverridable( - _DeleteTextAction(this, _linebreak)), + QuillEditorDeleteTextAction(this, _linebreak)), // Extend/Move Selection ExtendSelectionByCharacterIntent: _makeOverridable( - _UpdateTextSelectionAction( + QuillEditorUpdateTextSelectionAction( this, false, _characterBoundary, )), ExtendSelectionToNextWordBoundaryIntent: _makeOverridable( - _UpdateTextSelectionAction( + QuillEditorUpdateTextSelectionAction< + ExtendSelectionToNextWordBoundaryIntent>( this, true, _nextWordBoundary)), ExtendSelectionToLineBreakIntent: _makeOverridable( - _UpdateTextSelectionAction( + QuillEditorUpdateTextSelectionAction( this, true, _linebreak)), ExtendSelectionVerticallyToAdjacentLineIntent: _makeOverridable(_adjacentLineAction), ExtendSelectionToDocumentBoundaryIntent: _makeOverridable( - _UpdateTextSelectionAction( + QuillEditorUpdateTextSelectionAction< + ExtendSelectionToDocumentBoundaryIntent>( this, true, _documentBoundary)), ExtendSelectionToNextWordBoundaryOrCaretLocationIntent: _makeOverridable( - _ExtendSelectionOrCaretPositionAction(this, _nextWordBoundary)), + QuillEditorExtendSelectionOrCaretPositionAction( + this, _nextWordBoundary)), // Copy Paste - SelectAllTextIntent: _makeOverridable(_SelectAllAction(this)), - CopySelectionTextIntent: _makeOverridable(_CopySelectionAction(this)), + SelectAllTextIntent: _makeOverridable(QuillEditorSelectAllAction(this)), + CopySelectionTextIntent: + _makeOverridable(QuillEditorCopySelectionAction(this)), PasteTextIntent: _makeOverridable(CallbackAction( onInvoke: (intent) => pasteText(intent.cause))), HideSelectionToolbarIntent: - _makeOverridable(_HideSelectionToolbarAction(this)), - UndoTextIntent: _makeOverridable(_UndoKeyboardAction(this)), - RedoTextIntent: _makeOverridable(_RedoKeyboardAction(this)), + _makeOverridable(QuillEditorHideSelectionToolbarAction(this)), + UndoTextIntent: _makeOverridable(QuillEditorUndoKeyboardAction(this)), + RedoTextIntent: _makeOverridable(QuillEditorRedoKeyboardAction(this)), OpenSearchIntent: _openSearchAction, // Selection Formatting ToggleTextStyleIntent: _formatSelectionAction, IndentSelectionIntent: _indentSelectionAction, - ApplyHeaderIntent: _applyHeaderAction, - ApplyCheckListIntent: _applyCheckListAction, - ApplyLinkIntent: ApplyLinkAction(this) + QuillEditorApplyHeaderIntent: _applyHeaderAction, + QuillEditorApplyCheckListIntent: _applyCheckListAction, + QuillEditorApplyLinkIntent: QuillEditorApplyLinkAction(this) }; @override @@ -1848,955 +1626,3 @@ class RawEditorState extends EditorState @override bool get shareEnabled => false; } - -class _Editor extends MultiChildRenderObjectWidget { - const _Editor({ - required Key key, - required List children, - required this.document, - required this.textDirection, - required this.hasFocus, - required this.scrollable, - required this.selection, - required this.startHandleLayerLink, - required this.endHandleLayerLink, - required this.onSelectionChanged, - required this.onSelectionCompleted, - required this.scrollBottomInset, - required this.cursorController, - required this.floatingCursorDisabled, - this.padding = EdgeInsets.zero, - this.maxContentWidth, - this.offset, - }) : super(key: key, children: children); - - final ViewportOffset? offset; - final Document document; - final TextDirection textDirection; - final bool hasFocus; - final bool scrollable; - final TextSelection selection; - final LayerLink startHandleLayerLink; - final LayerLink endHandleLayerLink; - final TextSelectionChangedHandler onSelectionChanged; - final TextSelectionCompletedHandler onSelectionCompleted; - final double scrollBottomInset; - final EdgeInsetsGeometry padding; - final double? maxContentWidth; - final CursorCont cursorController; - final bool floatingCursorDisabled; - - @override - RenderEditor createRenderObject(BuildContext context) { - return RenderEditor( - offset: offset, - document: document, - textDirection: textDirection, - hasFocus: hasFocus, - scrollable: scrollable, - selection: selection, - startHandleLayerLink: startHandleLayerLink, - endHandleLayerLink: endHandleLayerLink, - onSelectionChanged: onSelectionChanged, - onSelectionCompleted: onSelectionCompleted, - cursorController: cursorController, - padding: padding, - maxContentWidth: maxContentWidth, - scrollBottomInset: scrollBottomInset, - floatingCursorDisabled: floatingCursorDisabled, - ); - } - - @override - void updateRenderObject( - BuildContext context, - covariant RenderEditor renderObject, - ) { - renderObject - ..offset = offset - ..document = document - ..setContainer(document.root) - ..textDirection = textDirection - ..setHasFocus(hasFocus) - ..setSelection(selection) - ..setStartHandleLayerLink(startHandleLayerLink) - ..setEndHandleLayerLink(endHandleLayerLink) - ..onSelectionChanged = onSelectionChanged - ..setScrollBottomInset(scrollBottomInset) - ..setPadding(padding) - ..maxContentWidth = maxContentWidth; - } -} - -/// An interface for retrieving the logical text boundary -/// (left-closed-right-open) -/// at a given location in a document. -/// -/// Depending on the implementation of the [_TextBoundary], the input -/// [TextPosition] can either point to a code unit, or a position between 2 code -/// units (which can be visually represented by the caret if the selection were -/// to collapse to that position). -/// -/// For example, [_LineBreak] interprets the input [TextPosition] as a caret -/// location, since in Flutter the caret is generally painted between the -/// character the [TextPosition] points to and its previous character, and -/// [_LineBreak] cares about the affinity of the input [TextPosition]. Most -/// other text boundaries however, interpret the input [TextPosition] as the -/// location of a code unit in the document, since it's easier to reason about -/// the text boundary given a code unit in the text. -/// -/// To convert a "code-unit-based" [_TextBoundary] to "caret-location-based", -/// use the [_CollapsedSelectionBoundary] combinator. -abstract class _TextBoundary { - const _TextBoundary(); - - TextEditingValue get textEditingValue; - - /// Returns the leading text boundary at the given location, inclusive. - TextPosition getLeadingTextBoundaryAt(TextPosition position); - - /// Returns the trailing text boundary at the given location, exclusive. - TextPosition getTrailingTextBoundaryAt(TextPosition position); - - TextRange getTextBoundaryAt(TextPosition position) { - return TextRange( - start: getLeadingTextBoundaryAt(position).offset, - end: getTrailingTextBoundaryAt(position).offset, - ); - } -} - -// ----------------------------- Text Boundaries ----------------------------- - -// The word modifier generally removes the word boundaries around white spaces -// (and newlines), IOW white spaces and some other punctuations are considered -// a part of the next word in the search direction. -class _WhitespaceBoundary extends _TextBoundary { - const _WhitespaceBoundary(this.textEditingValue); - - @override - final TextEditingValue textEditingValue; - - @override - TextPosition getLeadingTextBoundaryAt(TextPosition position) { - for (var index = position.offset; index >= 0; index -= 1) { - if (!TextLayoutMetrics.isWhitespace( - textEditingValue.text.codeUnitAt(index))) { - return TextPosition(offset: index); - } - } - return const TextPosition(offset: 0); - } - - @override - TextPosition getTrailingTextBoundaryAt(TextPosition position) { - for (var index = position.offset; - index < textEditingValue.text.length; - index += 1) { - if (!TextLayoutMetrics.isWhitespace( - textEditingValue.text.codeUnitAt(index))) { - return TextPosition(offset: index + 1); - } - } - return TextPosition(offset: textEditingValue.text.length); - } -} - -// Most apps delete the entire grapheme when the backspace key is pressed. -// Also always put the new caret location to character boundaries to avoid -// sending malformed UTF-16 code units to the paragraph builder. -class _CharacterBoundary extends _TextBoundary { - const _CharacterBoundary(this.textEditingValue); - - @override - final TextEditingValue textEditingValue; - - @override - TextPosition getLeadingTextBoundaryAt(TextPosition position) { - final int endOffset = - math.min(position.offset + 1, textEditingValue.text.length); - return TextPosition( - offset: - CharacterRange.at(textEditingValue.text, position.offset, endOffset) - .stringBeforeLength, - ); - } - - @override - TextPosition getTrailingTextBoundaryAt(TextPosition position) { - final int endOffset = - math.min(position.offset + 1, textEditingValue.text.length); - final range = - CharacterRange.at(textEditingValue.text, position.offset, endOffset); - return TextPosition( - offset: textEditingValue.text.length - range.stringAfterLength, - ); - } - - @override - TextRange getTextBoundaryAt(TextPosition position) { - final int endOffset = - math.min(position.offset + 1, textEditingValue.text.length); - final range = - CharacterRange.at(textEditingValue.text, position.offset, endOffset); - return TextRange( - start: range.stringBeforeLength, - end: textEditingValue.text.length - range.stringAfterLength, - ); - } -} - -// [UAX #29](https://unicode.org/reports/tr29/) defined word boundaries. -class _WordBoundary extends _TextBoundary { - const _WordBoundary(this.textLayout, this.textEditingValue); - - final TextLayoutMetrics textLayout; - - @override - final TextEditingValue textEditingValue; - - @override - TextPosition getLeadingTextBoundaryAt(TextPosition position) { - return TextPosition( - offset: textLayout.getWordBoundary(position).start, - // Word boundary seems to always report downstream on many platforms. - affinity: - TextAffinity.downstream, // ignore: avoid_redundant_argument_values - ); - } - - @override - TextPosition getTrailingTextBoundaryAt(TextPosition position) { - return TextPosition( - offset: textLayout.getWordBoundary(position).end, - // Word boundary seems to always report downstream on many platforms. - affinity: - TextAffinity.downstream, // ignore: avoid_redundant_argument_values - ); - } -} - -// The linebreaks of the current text layout. The input [TextPosition]s are -// interpreted as caret locations because [TextPainter.getLineAtOffset] is -// text-affinity-aware. -class _LineBreak extends _TextBoundary { - const _LineBreak(this.textLayout, this.textEditingValue); - - final TextLayoutMetrics textLayout; - - @override - final TextEditingValue textEditingValue; - - @override - TextPosition getLeadingTextBoundaryAt(TextPosition position) { - return TextPosition( - offset: textLayout.getLineAtOffset(position).start, - ); - } - - @override - TextPosition getTrailingTextBoundaryAt(TextPosition position) { - return TextPosition( - offset: textLayout.getLineAtOffset(position).end, - affinity: TextAffinity.upstream, - ); - } -} - -// The document boundary is unique and is a constant function of the input -// position. -class _DocumentBoundary extends _TextBoundary { - const _DocumentBoundary(this.textEditingValue); - - @override - final TextEditingValue textEditingValue; - - @override - TextPosition getLeadingTextBoundaryAt(TextPosition position) => - const TextPosition(offset: 0); - - @override - TextPosition getTrailingTextBoundaryAt(TextPosition position) { - return TextPosition( - offset: textEditingValue.text.length, - affinity: TextAffinity.upstream, - ); - } -} - -// ------------------------ Text Boundary Combinators ------------------------ - -// Expands the innerTextBoundary with outerTextBoundary. -class _ExpandedTextBoundary extends _TextBoundary { - _ExpandedTextBoundary(this.innerTextBoundary, this.outerTextBoundary); - - final _TextBoundary innerTextBoundary; - final _TextBoundary outerTextBoundary; - - @override - TextEditingValue get textEditingValue { - assert(innerTextBoundary.textEditingValue == - outerTextBoundary.textEditingValue); - return innerTextBoundary.textEditingValue; - } - - @override - TextPosition getLeadingTextBoundaryAt(TextPosition position) { - return outerTextBoundary.getLeadingTextBoundaryAt( - innerTextBoundary.getLeadingTextBoundaryAt(position), - ); - } - - @override - TextPosition getTrailingTextBoundaryAt(TextPosition position) { - return outerTextBoundary.getTrailingTextBoundaryAt( - innerTextBoundary.getTrailingTextBoundaryAt(position), - ); - } -} - -// Force the innerTextBoundary to interpret the input [TextPosition]s as caret -// locations instead of code unit positions. -// -// The innerTextBoundary must be a [_TextBoundary] that interprets the input -// [TextPosition]s as code unit positions. -class _CollapsedSelectionBoundary extends _TextBoundary { - _CollapsedSelectionBoundary(this.innerTextBoundary, this.isForward); - - final _TextBoundary innerTextBoundary; - final bool isForward; - - @override - TextEditingValue get textEditingValue => innerTextBoundary.textEditingValue; - - @override - TextPosition getLeadingTextBoundaryAt(TextPosition position) { - return isForward - ? innerTextBoundary.getLeadingTextBoundaryAt(position) - : position.offset <= 0 - ? const TextPosition(offset: 0) - : innerTextBoundary.getLeadingTextBoundaryAt( - TextPosition(offset: position.offset - 1)); - } - - @override - TextPosition getTrailingTextBoundaryAt(TextPosition position) { - return isForward - ? innerTextBoundary.getTrailingTextBoundaryAt(position) - : position.offset <= 0 - ? const TextPosition(offset: 0) - : innerTextBoundary.getTrailingTextBoundaryAt( - TextPosition(offset: position.offset - 1)); - } -} - -// A _TextBoundary that creates a [TextRange] where its start is from the -// specified leading text boundary and its end is from the specified trailing -// text boundary. -class _MixedBoundary extends _TextBoundary { - _MixedBoundary(this.leadingTextBoundary, this.trailingTextBoundary); - - final _TextBoundary leadingTextBoundary; - final _TextBoundary trailingTextBoundary; - - @override - TextEditingValue get textEditingValue { - assert(leadingTextBoundary.textEditingValue == - trailingTextBoundary.textEditingValue); - return leadingTextBoundary.textEditingValue; - } - - @override - TextPosition getLeadingTextBoundaryAt(TextPosition position) => - leadingTextBoundary.getLeadingTextBoundaryAt(position); - - @override - TextPosition getTrailingTextBoundaryAt(TextPosition position) => - trailingTextBoundary.getTrailingTextBoundaryAt(position); -} - -// ------------------------------- Text Actions ------------------------------- -class _DeleteTextAction - extends ContextAction { - _DeleteTextAction(this.state, this.getTextBoundariesForIntent); - - final RawEditorState state; - final _TextBoundary Function(T intent) getTextBoundariesForIntent; - - TextRange _expandNonCollapsedRange(TextEditingValue value) { - final TextRange selection = value.selection; - assert(selection.isValid); - assert(!selection.isCollapsed); - final _TextBoundary atomicBoundary = _CharacterBoundary(value); - - return TextRange( - start: atomicBoundary - .getLeadingTextBoundaryAt(TextPosition(offset: selection.start)) - .offset, - end: atomicBoundary - .getTrailingTextBoundaryAt(TextPosition(offset: selection.end - 1)) - .offset, - ); - } - - @override - Object? invoke(T intent, [BuildContext? context]) { - final selection = state.textEditingValue.selection; - assert(selection.isValid); - - if (!selection.isCollapsed) { - return Actions.invoke( - context!, - ReplaceTextIntent( - state.textEditingValue, - '', - _expandNonCollapsedRange(state.textEditingValue), - SelectionChangedCause.keyboard), - ); - } - - final textBoundary = getTextBoundariesForIntent(intent); - if (!textBoundary.textEditingValue.selection.isValid) { - return null; - } - if (!textBoundary.textEditingValue.selection.isCollapsed) { - return Actions.invoke( - context!, - ReplaceTextIntent( - state.textEditingValue, - '', - _expandNonCollapsedRange(textBoundary.textEditingValue), - SelectionChangedCause.keyboard), - ); - } - - return Actions.invoke( - context!, - ReplaceTextIntent( - textBoundary.textEditingValue, - '', - textBoundary - .getTextBoundaryAt(textBoundary.textEditingValue.selection.base), - SelectionChangedCause.keyboard, - ), - ); - } - - @override - bool get isActionEnabled => - !state.widget.readOnly && state.textEditingValue.selection.isValid; -} - -class _UpdateTextSelectionAction - extends ContextAction { - _UpdateTextSelectionAction(this.state, this.ignoreNonCollapsedSelection, - this.getTextBoundariesForIntent); - - final RawEditorState state; - final bool ignoreNonCollapsedSelection; - final _TextBoundary Function(T intent) getTextBoundariesForIntent; - - @override - Object? invoke(T intent, [BuildContext? context]) { - final selection = state.textEditingValue.selection; - assert(selection.isValid); - - final collapseSelection = - intent.collapseSelection || !state.widget.selectionEnabled; - // Collapse to the logical start/end. - TextSelection _collapse(TextSelection selection) { - assert(selection.isValid); - assert(!selection.isCollapsed); - return selection.copyWith( - baseOffset: intent.forward ? selection.end : selection.start, - extentOffset: intent.forward ? selection.end : selection.start, - ); - } - - if (!selection.isCollapsed && - !ignoreNonCollapsedSelection && - collapseSelection) { - return Actions.invoke( - context!, - UpdateSelectionIntent(state.textEditingValue, _collapse(selection), - SelectionChangedCause.keyboard), - ); - } - - final textBoundary = getTextBoundariesForIntent(intent); - final textBoundarySelection = textBoundary.textEditingValue.selection; - if (!textBoundarySelection.isValid) { - return null; - } - if (!textBoundarySelection.isCollapsed && - !ignoreNonCollapsedSelection && - collapseSelection) { - return Actions.invoke( - context!, - UpdateSelectionIntent(state.textEditingValue, - _collapse(textBoundarySelection), SelectionChangedCause.keyboard), - ); - } - - final extent = textBoundarySelection.extent; - final newExtent = intent.forward - ? textBoundary.getTrailingTextBoundaryAt(extent) - : textBoundary.getLeadingTextBoundaryAt(extent); - - final newSelection = collapseSelection - ? TextSelection.fromPosition(newExtent) - : textBoundarySelection.extendTo(newExtent); - - // If collapseAtReversal is true and would have an effect, collapse it. - if (!selection.isCollapsed && - intent.collapseAtReversal && - (selection.baseOffset < selection.extentOffset != - newSelection.baseOffset < newSelection.extentOffset)) { - return Actions.invoke( - context!, - UpdateSelectionIntent( - state.textEditingValue, - TextSelection.fromPosition(selection.base), - SelectionChangedCause.keyboard, - ), - ); - } - - return Actions.invoke( - context!, - UpdateSelectionIntent(textBoundary.textEditingValue, newSelection, - SelectionChangedCause.keyboard), - ); - } - - @override - bool get isActionEnabled => state.textEditingValue.selection.isValid; -} - -class _ExtendSelectionOrCaretPositionAction extends ContextAction< - ExtendSelectionToNextWordBoundaryOrCaretLocationIntent> { - _ExtendSelectionOrCaretPositionAction( - this.state, this.getTextBoundariesForIntent); - - final RawEditorState state; - final _TextBoundary Function( - ExtendSelectionToNextWordBoundaryOrCaretLocationIntent intent) - getTextBoundariesForIntent; - - @override - Object? invoke(ExtendSelectionToNextWordBoundaryOrCaretLocationIntent intent, - [BuildContext? context]) { - final selection = state.textEditingValue.selection; - assert(selection.isValid); - - final textBoundary = getTextBoundariesForIntent(intent); - final textBoundarySelection = textBoundary.textEditingValue.selection; - if (!textBoundarySelection.isValid) { - return null; - } - - final extent = textBoundarySelection.extent; - final newExtent = intent.forward - ? textBoundary.getTrailingTextBoundaryAt(extent) - : textBoundary.getLeadingTextBoundaryAt(extent); - - final newSelection = (newExtent.offset - textBoundarySelection.baseOffset) * - (textBoundarySelection.extentOffset - - textBoundarySelection.baseOffset) < - 0 - ? textBoundarySelection.copyWith( - extentOffset: textBoundarySelection.baseOffset, - affinity: textBoundarySelection.extentOffset > - textBoundarySelection.baseOffset - ? TextAffinity.downstream - : TextAffinity.upstream, - ) - : textBoundarySelection.extendTo(newExtent); - - return Actions.invoke( - context!, - UpdateSelectionIntent(textBoundary.textEditingValue, newSelection, - SelectionChangedCause.keyboard), - ); - } - - @override - bool get isActionEnabled => - state.widget.selectionEnabled && state.textEditingValue.selection.isValid; -} - -class _UpdateTextSelectionToAdjacentLineAction< - T extends DirectionalCaretMovementIntent> extends ContextAction { - _UpdateTextSelectionToAdjacentLineAction(this.state); - - final RawEditorState state; - - QuillVerticalCaretMovementRun? _verticalMovementRun; - TextSelection? _runSelection; - - void stopCurrentVerticalRunIfSelectionChanges() { - final runSelection = _runSelection; - if (runSelection == null) { - assert(_verticalMovementRun == null); - return; - } - _runSelection = state.textEditingValue.selection; - final currentSelection = state.controller.selection; - final continueCurrentRun = currentSelection.isValid && - currentSelection.isCollapsed && - currentSelection.baseOffset == runSelection.baseOffset && - currentSelection.extentOffset == runSelection.extentOffset; - if (!continueCurrentRun) { - _verticalMovementRun = null; - _runSelection = null; - } - } - - @override - void invoke(T intent, [BuildContext? context]) { - assert(state.textEditingValue.selection.isValid); - - final collapseSelection = - intent.collapseSelection || !state.widget.selectionEnabled; - final value = state.textEditingValue; - if (!value.selection.isValid) { - return; - } - - final currentRun = _verticalMovementRun ?? - state.renderEditor - .startVerticalCaretMovement(state.renderEditor.selection.extent); - - final shouldMove = - intent.forward ? currentRun.moveNext() : currentRun.movePrevious(); - final newExtent = shouldMove - ? currentRun.current - : (intent.forward - ? TextPosition(offset: state.textEditingValue.text.length) - : const TextPosition(offset: 0)); - final newSelection = collapseSelection - ? TextSelection.fromPosition(newExtent) - : value.selection.extendTo(newExtent); - - Actions.invoke( - context!, - UpdateSelectionIntent( - value, newSelection, SelectionChangedCause.keyboard), - ); - if (state.textEditingValue.selection == newSelection) { - _verticalMovementRun = currentRun; - _runSelection = newSelection; - } - } - - @override - bool get isActionEnabled => state.textEditingValue.selection.isValid; -} - -class _SelectAllAction extends ContextAction { - _SelectAllAction(this.state); - - final RawEditorState state; - - @override - Object? invoke(SelectAllTextIntent intent, [BuildContext? context]) { - return Actions.invoke( - context!, - UpdateSelectionIntent( - state.textEditingValue, - TextSelection( - baseOffset: 0, extentOffset: state.textEditingValue.text.length), - intent.cause, - ), - ); - } - - @override - bool get isActionEnabled => state.widget.selectionEnabled; -} - -class _CopySelectionAction extends ContextAction { - _CopySelectionAction(this.state); - - final RawEditorState state; - - @override - void invoke(CopySelectionTextIntent intent, [BuildContext? context]) { - if (intent.collapseSelection) { - state.cutSelection(intent.cause); - } else { - state.copySelection(intent.cause); - } - } - - @override - bool get isActionEnabled => - state.textEditingValue.selection.isValid && - !state.textEditingValue.selection.isCollapsed; -} - -//Intent class for "escape" key to dismiss selection toolbar in Windows platform -class HideSelectionToolbarIntent extends Intent { - const HideSelectionToolbarIntent(); -} - -class _HideSelectionToolbarAction - extends ContextAction { - _HideSelectionToolbarAction(this.state); - - final RawEditorState state; - - @override - void invoke(HideSelectionToolbarIntent intent, [BuildContext? context]) { - state.hideToolbar(); - } - - @override - bool get isActionEnabled => state.textEditingValue.selection.isValid; -} - -class _UndoKeyboardAction extends ContextAction { - _UndoKeyboardAction(this.state); - - final RawEditorState state; - - @override - void invoke(UndoTextIntent intent, [BuildContext? context]) { - if (state.controller.hasUndo) { - state.controller.undo(); - } - } - - @override - bool get isActionEnabled => true; -} - -class _RedoKeyboardAction extends ContextAction { - _RedoKeyboardAction(this.state); - - final RawEditorState state; - - @override - void invoke(RedoTextIntent intent, [BuildContext? context]) { - if (state.controller.hasRedo) { - state.controller.redo(); - } - } - - @override - bool get isActionEnabled => true; -} - -class ToggleTextStyleIntent extends Intent { - const ToggleTextStyleIntent(this.attribute); - - final Attribute attribute; -} - -// Toggles a text style (underline, bold, italic, strikethrough) on, or off. -class _ToggleTextStyleAction extends Action { - _ToggleTextStyleAction(this.state); - - final RawEditorState state; - - bool _isStyleActive(Attribute styleAttr, Map attrs) { - if (styleAttr.key == Attribute.list.key) { - final attribute = attrs[styleAttr.key]; - if (attribute == null) { - return false; - } - return attribute.value == styleAttr.value; - } - return attrs.containsKey(styleAttr.key); - } - - @override - void invoke(ToggleTextStyleIntent intent, [BuildContext? context]) { - final isActive = _isStyleActive( - intent.attribute, state.controller.getSelectionStyle().attributes); - state.controller.formatSelection( - isActive ? Attribute.clone(intent.attribute, null) : intent.attribute); - } - - @override - bool get isActionEnabled => true; -} - -class IndentSelectionIntent extends Intent { - const IndentSelectionIntent(this.isIncrease); - - final bool isIncrease; -} - -// Toggles a text style (underline, bold, italic, strikethrough) on, or off. -class _IndentSelectionAction extends Action { - _IndentSelectionAction(this.state); - - final RawEditorState state; - - @override - void invoke(IndentSelectionIntent intent, [BuildContext? context]) { - state.controller.indentSelection(intent.isIncrease); - } - - @override - bool get isActionEnabled => true; -} - -class OpenSearchIntent extends Intent { - const OpenSearchIntent(); -} - -// Toggles a text style (underline, bold, italic, strikethrough) on, or off. -class _OpenSearchAction extends ContextAction { - _OpenSearchAction(this.state); - - final RawEditorState state; - - @override - Future invoke(OpenSearchIntent intent, [BuildContext? context]) async { - if (context == null) { - throw ArgumentError( - 'The context should not be null to use invoke() method', - ); - } - await showDialog( - context: context, - builder: (_) => QuillToolbarSearchDialog( - controller: state.controller, - text: '', - ), - ); - } - - @override - bool get isActionEnabled => true; -} - -class ApplyHeaderIntent extends Intent { - const ApplyHeaderIntent(this.header); - - final Attribute header; -} - -// Toggles a text style (underline, bold, italic, strikethrough) on, or off. -class _ApplyHeaderAction extends Action { - _ApplyHeaderAction(this.state); - - final RawEditorState state; - - Attribute _getHeaderValue() { - return state.controller - .getSelectionStyle() - .attributes[Attribute.header.key] ?? - Attribute.header; - } - - @override - void invoke(ApplyHeaderIntent intent, [BuildContext? context]) { - final _attribute = - _getHeaderValue() == intent.header ? Attribute.header : intent.header; - state.controller.formatSelection(_attribute); - } - - @override - bool get isActionEnabled => true; -} - -class ApplyCheckListIntent extends Intent { - const ApplyCheckListIntent(); -} - -// Toggles a text style (underline, bold, italic, strikethrough) on, or off. -class _ApplyCheckListAction extends Action { - _ApplyCheckListAction(this.state); - - final RawEditorState state; - - bool _getIsToggled() { - final attrs = state.controller.getSelectionStyle().attributes; - var attribute = state.controller.toolbarButtonToggler[Attribute.list.key]; - - if (attribute == null) { - attribute = attrs[Attribute.list.key]; - } else { - // checkbox tapping causes controller.selection to go to offset 0 - state.controller.toolbarButtonToggler.remove(Attribute.list.key); - } - - if (attribute == null) { - return false; - } - return attribute.value == Attribute.unchecked.value || - attribute.value == Attribute.checked.value; - } - - @override - void invoke(ApplyCheckListIntent intent, [BuildContext? context]) { - state.controller.formatSelection(_getIsToggled() - ? Attribute.clone(Attribute.unchecked, null) - : Attribute.unchecked); - } - - @override - bool get isActionEnabled => true; -} - -class ApplyLinkIntent extends Intent { - const ApplyLinkIntent(); -} - -class ApplyLinkAction extends Action { - ApplyLinkAction(this.state); - - final RawEditorState state; - - @override - Object? invoke(ApplyLinkIntent intent) async { - final initialTextLink = QuillTextLink.prepare(state.controller); - - final textLink = await showDialog( - context: state.context, - builder: (context) { - return LinkStyleDialog( - text: initialTextLink.text, - link: initialTextLink.link, - dialogTheme: state.widget.dialogTheme, - ); - }, - ); - - if (textLink != null) { - textLink.submit(state.controller); - } - return null; - } -} - -class InsertEmbedIntent extends Intent { - const InsertEmbedIntent(this.type); - - final Attribute type; -} - -/// Signature for a widget builder that builds a context menu for the given -/// [RawEditorState]. -/// -/// See also: -/// -/// * [EditableTextContextMenuBuilder], which performs the same role for -/// [EditableText] -typedef QuillEditorContextMenuBuilder = Widget Function( - BuildContext context, - RawEditorState rawEditorState, -); - -class _GlyphHeights { - _GlyphHeights( - this.startGlyphHeight, - this.endGlyphHeight, - ); - - final double startGlyphHeight; - final double endGlyphHeight; -} diff --git a/lib/src/widgets/raw_editor/raw_editor_actions.dart b/lib/src/widgets/raw_editor/raw_editor_actions.dart new file mode 100644 index 00000000..54a7eba7 --- /dev/null +++ b/lib/src/widgets/raw_editor/raw_editor_actions.dart @@ -0,0 +1,580 @@ +import 'package:flutter/material.dart'; + +import '../../models/documents/attribute.dart'; +import '../editor/editor.dart'; +import '../toolbar/buttons/link_style2.dart'; +import '../toolbar/buttons/search/search_dialog.dart'; +import 'raw_editor_state.dart'; +import 'raw_editor_text_boundaries.dart'; + +// ------------------------------- Text Actions ------------------------------- +class QuillEditorDeleteTextAction + extends ContextAction { + QuillEditorDeleteTextAction(this.state, this.getTextBoundariesForIntent); + + final QuillRawEditorState state; + final QuillEditorTextBoundary Function(T intent) getTextBoundariesForIntent; + + TextRange _expandNonCollapsedRange(TextEditingValue value) { + final TextRange selection = value.selection; + assert(selection.isValid); + assert(!selection.isCollapsed); + final atomicBoundary = QuillEditorCharacterBoundary(value); + + return TextRange( + start: atomicBoundary + .getLeadingTextBoundaryAt(TextPosition(offset: selection.start)) + .offset, + end: atomicBoundary + .getTrailingTextBoundaryAt(TextPosition(offset: selection.end - 1)) + .offset, + ); + } + + @override + Object? invoke(T intent, [BuildContext? context]) { + final selection = state.textEditingValue.selection; + assert(selection.isValid); + + if (!selection.isCollapsed) { + return Actions.invoke( + context!, + ReplaceTextIntent( + state.textEditingValue, + '', + _expandNonCollapsedRange(state.textEditingValue), + SelectionChangedCause.keyboard), + ); + } + + final textBoundary = getTextBoundariesForIntent(intent); + if (!textBoundary.textEditingValue.selection.isValid) { + return null; + } + if (!textBoundary.textEditingValue.selection.isCollapsed) { + return Actions.invoke( + context!, + ReplaceTextIntent( + state.textEditingValue, + '', + _expandNonCollapsedRange(textBoundary.textEditingValue), + SelectionChangedCause.keyboard), + ); + } + + return Actions.invoke( + context!, + ReplaceTextIntent( + textBoundary.textEditingValue, + '', + textBoundary + .getTextBoundaryAt(textBoundary.textEditingValue.selection.base), + SelectionChangedCause.keyboard, + ), + ); + } + + @override + bool get isActionEnabled => + !state.widget.configurations.isReadOnly && + state.textEditingValue.selection.isValid; +} + +class QuillEditorUpdateTextSelectionAction< + T extends DirectionalCaretMovementIntent> extends ContextAction { + QuillEditorUpdateTextSelectionAction(this.state, + this.ignoreNonCollapsedSelection, this.getTextBoundariesForIntent); + + final QuillRawEditorState state; + final bool ignoreNonCollapsedSelection; + final QuillEditorTextBoundary Function(T intent) getTextBoundariesForIntent; + + @override + Object? invoke(T intent, [BuildContext? context]) { + final selection = state.textEditingValue.selection; + assert(selection.isValid); + + final collapseSelection = intent.collapseSelection || + !state.widget.configurations.selectionEnabled; + // Collapse to the logical start/end. + TextSelection collapse(TextSelection selection) { + assert(selection.isValid); + assert(!selection.isCollapsed); + return selection.copyWith( + baseOffset: intent.forward ? selection.end : selection.start, + extentOffset: intent.forward ? selection.end : selection.start, + ); + } + + if (!selection.isCollapsed && + !ignoreNonCollapsedSelection && + collapseSelection) { + return Actions.invoke( + context!, + UpdateSelectionIntent( + state.textEditingValue, + collapse(selection), + SelectionChangedCause.keyboard, + ), + ); + } + + final textBoundary = getTextBoundariesForIntent(intent); + final textBoundarySelection = textBoundary.textEditingValue.selection; + if (!textBoundarySelection.isValid) { + return null; + } + if (!textBoundarySelection.isCollapsed && + !ignoreNonCollapsedSelection && + collapseSelection) { + return Actions.invoke( + context!, + UpdateSelectionIntent(state.textEditingValue, + collapse(textBoundarySelection), SelectionChangedCause.keyboard), + ); + } + + final extent = textBoundarySelection.extent; + final newExtent = intent.forward + ? textBoundary.getTrailingTextBoundaryAt(extent) + : textBoundary.getLeadingTextBoundaryAt(extent); + + final newSelection = collapseSelection + ? TextSelection.fromPosition(newExtent) + : textBoundarySelection.extendTo(newExtent); + + // If collapseAtReversal is true and would have an effect, collapse it. + if (!selection.isCollapsed && + intent.collapseAtReversal && + (selection.baseOffset < selection.extentOffset != + newSelection.baseOffset < newSelection.extentOffset)) { + return Actions.invoke( + context!, + UpdateSelectionIntent( + state.textEditingValue, + TextSelection.fromPosition(selection.base), + SelectionChangedCause.keyboard, + ), + ); + } + + return Actions.invoke( + context!, + UpdateSelectionIntent(textBoundary.textEditingValue, newSelection, + SelectionChangedCause.keyboard), + ); + } + + @override + bool get isActionEnabled => state.textEditingValue.selection.isValid; +} + +class QuillEditorExtendSelectionOrCaretPositionAction extends ContextAction< + ExtendSelectionToNextWordBoundaryOrCaretLocationIntent> { + QuillEditorExtendSelectionOrCaretPositionAction( + this.state, this.getTextBoundariesForIntent); + + final QuillRawEditorState state; + final QuillEditorTextBoundary Function( + ExtendSelectionToNextWordBoundaryOrCaretLocationIntent intent) + getTextBoundariesForIntent; + + @override + Object? invoke(ExtendSelectionToNextWordBoundaryOrCaretLocationIntent intent, + [BuildContext? context]) { + final selection = state.textEditingValue.selection; + assert(selection.isValid); + + final textBoundary = getTextBoundariesForIntent(intent); + final textBoundarySelection = textBoundary.textEditingValue.selection; + if (!textBoundarySelection.isValid) { + return null; + } + + final extent = textBoundarySelection.extent; + final newExtent = intent.forward + ? textBoundary.getTrailingTextBoundaryAt(extent) + : textBoundary.getLeadingTextBoundaryAt(extent); + + final newSelection = (newExtent.offset - textBoundarySelection.baseOffset) * + (textBoundarySelection.extentOffset - + textBoundarySelection.baseOffset) < + 0 + ? textBoundarySelection.copyWith( + extentOffset: textBoundarySelection.baseOffset, + affinity: textBoundarySelection.extentOffset > + textBoundarySelection.baseOffset + ? TextAffinity.downstream + : TextAffinity.upstream, + ) + : textBoundarySelection.extendTo(newExtent); + + return Actions.invoke( + context!, + UpdateSelectionIntent(textBoundary.textEditingValue, newSelection, + SelectionChangedCause.keyboard), + ); + } + + @override + bool get isActionEnabled => + state.widget.configurations.selectionEnabled && + state.textEditingValue.selection.isValid; +} + +class QuillEditorUpdateTextSelectionToAdjacentLineAction< + T extends DirectionalCaretMovementIntent> extends ContextAction { + QuillEditorUpdateTextSelectionToAdjacentLineAction(this.state); + + final QuillRawEditorState state; + + QuillVerticalCaretMovementRun? _verticalMovementRun; + TextSelection? _runSelection; + + void stopCurrentVerticalRunIfSelectionChanges() { + final runSelection = _runSelection; + if (runSelection == null) { + assert(_verticalMovementRun == null); + return; + } + _runSelection = state.textEditingValue.selection; + final currentSelection = state.controller.selection; + final continueCurrentRun = currentSelection.isValid && + currentSelection.isCollapsed && + currentSelection.baseOffset == runSelection.baseOffset && + currentSelection.extentOffset == runSelection.extentOffset; + if (!continueCurrentRun) { + _verticalMovementRun = null; + _runSelection = null; + } + } + + @override + void invoke(T intent, [BuildContext? context]) { + assert(state.textEditingValue.selection.isValid); + + final collapseSelection = intent.collapseSelection || + !state.widget.configurations.selectionEnabled; + final value = state.textEditingValue; + if (!value.selection.isValid) { + return; + } + + final currentRun = _verticalMovementRun ?? + state.renderEditor + .startVerticalCaretMovement(state.renderEditor.selection.extent); + + final shouldMove = + intent.forward ? currentRun.moveNext() : currentRun.movePrevious(); + final newExtent = shouldMove + ? currentRun.current + : (intent.forward + ? TextPosition(offset: state.textEditingValue.text.length) + : const TextPosition(offset: 0)); + final newSelection = collapseSelection + ? TextSelection.fromPosition(newExtent) + : value.selection.extendTo(newExtent); + + Actions.invoke( + context!, + UpdateSelectionIntent( + value, newSelection, SelectionChangedCause.keyboard), + ); + if (state.textEditingValue.selection == newSelection) { + _verticalMovementRun = currentRun; + _runSelection = newSelection; + } + } + + @override + bool get isActionEnabled => state.textEditingValue.selection.isValid; +} + +class QuillEditorSelectAllAction extends ContextAction { + QuillEditorSelectAllAction(this.state); + + final QuillRawEditorState state; + + @override + Object? invoke(SelectAllTextIntent intent, [BuildContext? context]) { + return Actions.invoke( + context!, + UpdateSelectionIntent( + state.textEditingValue, + TextSelection( + baseOffset: 0, extentOffset: state.textEditingValue.text.length), + intent.cause, + ), + ); + } + + @override + bool get isActionEnabled => state.widget.configurations.selectionEnabled; +} + +class QuillEditorCopySelectionAction + extends ContextAction { + QuillEditorCopySelectionAction(this.state); + + final QuillRawEditorState state; + + @override + void invoke(CopySelectionTextIntent intent, [BuildContext? context]) { + if (intent.collapseSelection) { + state.cutSelection(intent.cause); + } else { + state.copySelection(intent.cause); + } + } + + @override + bool get isActionEnabled => + state.textEditingValue.selection.isValid && + !state.textEditingValue.selection.isCollapsed; +} + +//Intent class for "escape" key to dismiss selection toolbar in Windows platform +class HideSelectionToolbarIntent extends Intent { + const HideSelectionToolbarIntent(); +} + +class QuillEditorHideSelectionToolbarAction + extends ContextAction { + QuillEditorHideSelectionToolbarAction(this.state); + + final QuillRawEditorState state; + + @override + void invoke(HideSelectionToolbarIntent intent, [BuildContext? context]) { + state.hideToolbar(); + } + + @override + bool get isActionEnabled => state.textEditingValue.selection.isValid; +} + +class QuillEditorUndoKeyboardAction extends ContextAction { + QuillEditorUndoKeyboardAction(this.state); + + final QuillRawEditorState state; + + @override + void invoke(UndoTextIntent intent, [BuildContext? context]) { + if (state.controller.hasUndo) { + state.controller.undo(); + } + } + + @override + bool get isActionEnabled => true; +} + +class QuillEditorRedoKeyboardAction extends ContextAction { + QuillEditorRedoKeyboardAction(this.state); + + final QuillRawEditorState state; + + @override + void invoke(RedoTextIntent intent, [BuildContext? context]) { + if (state.controller.hasRedo) { + state.controller.redo(); + } + } + + @override + bool get isActionEnabled => true; +} + +class ToggleTextStyleIntent extends Intent { + const ToggleTextStyleIntent(this.attribute); + + final Attribute attribute; +} + +// Toggles a text style (underline, bold, italic, strikethrough) on, or off. +class QuillEditorToggleTextStyleAction extends Action { + QuillEditorToggleTextStyleAction(this.state); + + final QuillRawEditorState state; + + bool _isStyleActive(Attribute styleAttr, Map attrs) { + if (styleAttr.key == Attribute.list.key) { + final attribute = attrs[styleAttr.key]; + if (attribute == null) { + return false; + } + return attribute.value == styleAttr.value; + } + return attrs.containsKey(styleAttr.key); + } + + @override + void invoke(ToggleTextStyleIntent intent, [BuildContext? context]) { + final isActive = _isStyleActive( + intent.attribute, state.controller.getSelectionStyle().attributes); + state.controller.formatSelection( + isActive ? Attribute.clone(intent.attribute, null) : intent.attribute); + } + + @override + bool get isActionEnabled => true; +} + +class IndentSelectionIntent extends Intent { + const IndentSelectionIntent(this.isIncrease); + + final bool isIncrease; +} + +// Toggles a text style (underline, bold, italic, strikethrough) on, or off. +class QuillEditorIndentSelectionAction extends Action { + QuillEditorIndentSelectionAction(this.state); + + final QuillRawEditorState state; + + @override + void invoke(IndentSelectionIntent intent, [BuildContext? context]) { + state.controller.indentSelection(intent.isIncrease); + } + + @override + bool get isActionEnabled => true; +} + +class OpenSearchIntent extends Intent { + const OpenSearchIntent(); +} + +// Toggles a text style (underline, bold, italic, strikethrough) on, or off. +class QuillEditorOpenSearchAction extends ContextAction { + QuillEditorOpenSearchAction(this.state); + + final QuillRawEditorState state; + + @override + Future invoke(OpenSearchIntent intent, [BuildContext? context]) async { + if (context == null) { + throw ArgumentError( + 'The context should not be null to use invoke() method', + ); + } + await showDialog( + context: context, + builder: (_) => QuillToolbarSearchDialog( + controller: state.controller, + text: '', + ), + ); + } + + @override + bool get isActionEnabled => true; +} + +class QuillEditorApplyHeaderIntent extends Intent { + const QuillEditorApplyHeaderIntent(this.header); + + final Attribute header; +} + +// Toggles a text style (underline, bold, italic, strikethrough) on, or off. +class QuillEditorApplyHeaderAction + extends Action { + QuillEditorApplyHeaderAction(this.state); + + final QuillRawEditorState state; + + Attribute _getHeaderValue() { + return state.controller + .getSelectionStyle() + .attributes[Attribute.header.key] ?? + Attribute.header; + } + + @override + void invoke(QuillEditorApplyHeaderIntent intent, [BuildContext? context]) { + final attribute = + _getHeaderValue() == intent.header ? Attribute.header : intent.header; + state.controller.formatSelection(attribute); + } + + @override + bool get isActionEnabled => true; +} + +class QuillEditorApplyCheckListIntent extends Intent { + const QuillEditorApplyCheckListIntent(); +} + +// Toggles a text style (underline, bold, italic, strikethrough) on, or off. +class QuillEditorApplyCheckListAction + extends Action { + QuillEditorApplyCheckListAction(this.state); + + final QuillRawEditorState state; + + bool _getIsToggled() { + final attrs = state.controller.getSelectionStyle().attributes; + var attribute = state.controller.toolbarButtonToggler[Attribute.list.key]; + + if (attribute == null) { + attribute = attrs[Attribute.list.key]; + } else { + // checkbox tapping causes controller.selection to go to offset 0 + state.controller.toolbarButtonToggler.remove(Attribute.list.key); + } + + if (attribute == null) { + return false; + } + return attribute.value == Attribute.unchecked.value || + attribute.value == Attribute.checked.value; + } + + @override + void invoke(QuillEditorApplyCheckListIntent intent, [BuildContext? context]) { + state.controller.formatSelection(_getIsToggled() + ? Attribute.clone(Attribute.unchecked, null) + : Attribute.unchecked); + } + + @override + bool get isActionEnabled => true; +} + +class QuillEditorApplyLinkIntent extends Intent { + const QuillEditorApplyLinkIntent(); +} + +class QuillEditorApplyLinkAction extends Action { + QuillEditorApplyLinkAction(this.state); + + final QuillRawEditorState state; + + @override + Object? invoke(QuillEditorApplyLinkIntent intent) async { + final initialTextLink = QuillTextLink.prepare(state.controller); + + final textLink = await showDialog( + context: state.context, + builder: (context) { + return LinkStyleDialog( + text: initialTextLink.text, + link: initialTextLink.link, + dialogTheme: state.widget.configurations.dialogTheme, + ); + }, + ); + + if (textLink != null) { + textLink.submit(state.controller); + } + return null; + } +} + +class QuillEditorInsertEmbedIntent extends Intent { + const QuillEditorInsertEmbedIntent(this.type); + + final Attribute type; +} diff --git a/lib/src/widgets/raw_editor/raw_editor_render_object.dart b/lib/src/widgets/raw_editor/raw_editor_render_object.dart new file mode 100644 index 00000000..3338c1f2 --- /dev/null +++ b/lib/src/widgets/raw_editor/raw_editor_render_object.dart @@ -0,0 +1,85 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart' show ViewportOffset; + +import '../../models/documents/document.dart'; +import '../cursor.dart'; +import '../editor/editor.dart'; + +class QuilRawEditorMultiChildRenderObject extends MultiChildRenderObjectWidget { + const QuilRawEditorMultiChildRenderObject({ + required super.children, + required this.document, + required this.textDirection, + required this.hasFocus, + required this.scrollable, + required this.selection, + required this.startHandleLayerLink, + required this.endHandleLayerLink, + required this.onSelectionChanged, + required this.onSelectionCompleted, + required this.scrollBottomInset, + required this.cursorController, + required this.floatingCursorDisabled, + super.key, + this.padding = EdgeInsets.zero, + this.maxContentWidth, + this.offset, + }); + + final ViewportOffset? offset; + final Document document; + final TextDirection textDirection; + final bool hasFocus; + final bool scrollable; + final TextSelection selection; + final LayerLink startHandleLayerLink; + final LayerLink endHandleLayerLink; + final TextSelectionChangedHandler onSelectionChanged; + final TextSelectionCompletedHandler onSelectionCompleted; + final double scrollBottomInset; + final EdgeInsetsGeometry padding; + final double? maxContentWidth; + final CursorCont cursorController; + final bool floatingCursorDisabled; + + @override + RenderEditor createRenderObject(BuildContext context) { + return RenderEditor( + offset: offset, + document: document, + textDirection: textDirection, + hasFocus: hasFocus, + scrollable: scrollable, + selection: selection, + startHandleLayerLink: startHandleLayerLink, + endHandleLayerLink: endHandleLayerLink, + onSelectionChanged: onSelectionChanged, + onSelectionCompleted: onSelectionCompleted, + cursorController: cursorController, + padding: padding, + maxContentWidth: maxContentWidth, + scrollBottomInset: scrollBottomInset, + floatingCursorDisabled: floatingCursorDisabled, + ); + } + + @override + void updateRenderObject( + BuildContext context, + covariant RenderEditor renderObject, + ) { + renderObject + ..offset = offset + ..document = document + ..setContainer(document.root) + ..textDirection = textDirection + ..setHasFocus(hasFocus) + ..setSelection(selection) + ..setStartHandleLayerLink(startHandleLayerLink) + ..setEndHandleLayerLink(endHandleLayerLink) + ..onSelectionChanged = onSelectionChanged + ..setScrollBottomInset(scrollBottomInset) + ..setPadding(padding) + ..maxContentWidth = maxContentWidth; + } +} diff --git a/lib/src/widgets/raw_editor/raw_editor_state.dart b/lib/src/widgets/raw_editor/raw_editor_state.dart new file mode 100644 index 00000000..0c6d6ee6 --- /dev/null +++ b/lib/src/widgets/raw_editor/raw_editor_state.dart @@ -0,0 +1,1617 @@ +import 'dart:async' show StreamSubscription; +import 'dart:convert' show jsonDecode; +import 'dart:math' as math; +import 'dart:ui' as ui hide TextStyle; + +import 'package:collection/collection.dart'; +import 'package:flutter/foundation.dart' show defaultTargetPlatform; +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart' show RenderAbstractViewport; +import 'package:flutter/scheduler.dart' show SchedulerBinding; +import 'package:flutter/services.dart' + show + LogicalKeyboardKey, + RawKeyDownEvent, + HardwareKeyboard, + Clipboard, + ClipboardData, + TextInputControl; +import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart' + show KeyboardVisibilityController; +import 'package:pasteboard/pasteboard.dart' show Pasteboard; + +import '../../models/documents/attribute.dart'; +import '../../models/documents/document.dart'; +import '../../models/documents/nodes/block.dart'; +import '../../models/documents/nodes/embeddable.dart'; +import '../../models/documents/nodes/leaf.dart' as leaf; +import '../../models/documents/nodes/line.dart'; +import '../../models/documents/nodes/node.dart'; +import '../../models/structs/offset_value.dart'; +import '../../models/structs/vertical_spacing.dart'; +import '../../utils/cast.dart'; +import '../../utils/delta.dart'; +import '../../utils/embeds.dart'; +import '../../utils/platform.dart'; +import '../controller.dart'; +import '../cursor.dart'; +import '../default_styles.dart'; +import '../editor/editor.dart'; +import '../keyboard_listener.dart'; +import '../link.dart'; +import '../proxy.dart'; +import '../quill_single_child_scroll_view.dart'; +import '../text_block.dart'; +import '../text_line.dart'; +import '../text_selection.dart'; +import 'raw_editor.dart'; +import 'raw_editor_actions.dart'; +import 'raw_editor_render_object.dart'; +import 'raw_editor_state_selection_delegate_mixin.dart'; +import 'raw_editor_state_text_input_client_mixin.dart'; +import 'raw_editor_text_boundaries.dart'; + +class QuillRawEditorState extends EditorState + with + AutomaticKeepAliveClientMixin, + WidgetsBindingObserver, + TickerProviderStateMixin, + RawEditorStateTextInputClientMixin, + RawEditorStateSelectionDelegateMixin { + final GlobalKey _editorKey = GlobalKey(); + + KeyboardVisibilityController? _keyboardVisibilityController; + StreamSubscription? _keyboardVisibilitySubscription; + bool _keyboardVisible = false; + + // Selection overlay + @override + EditorTextSelectionOverlay? get selectionOverlay => _selectionOverlay; + EditorTextSelectionOverlay? _selectionOverlay; + + @override + ScrollController get scrollController => _scrollController; + late ScrollController _scrollController; + + // Cursors + late CursorCont _cursorCont; + + QuillController get controller => widget.configurations.controller; + + // Focus + bool _didAutoFocus = false; + + bool get _hasFocus => widget.configurations.focusNode.hasFocus; + + // Theme + DefaultStyles? _styles; + + // for pasting style + @override + List get pasteStyleAndEmbed => _pasteStyleAndEmbed; + List _pasteStyleAndEmbed = []; + + @override + String get pastePlainText => _pastePlainText; + String _pastePlainText = ''; + + final ClipboardStatusNotifier _clipboardStatus = ClipboardStatusNotifier(); + final LayerLink _toolbarLayerLink = LayerLink(); + final LayerLink _startHandleLayerLink = LayerLink(); + final LayerLink _endHandleLayerLink = LayerLink(); + + TextDirection get _textDirection => Directionality.of(context); + + @override + bool get dirty => _dirty; + bool _dirty = false; + + @override + void insertContent(KeyboardInsertedContent content) { + assert(widget.configurations.contentInsertionConfiguration?.allowedMimeTypes + .contains(content.mimeType) ?? + false); + widget.configurations.contentInsertionConfiguration?.onContentInserted + .call(content); + } + + /// Returns the [ContextMenuButtonItem]s representing the buttons in this + /// platform's default selection menu for [QuillRawEditor]. + /// + /// Copied from [EditableTextState]. + List get contextMenuButtonItems { + return EditableText.getEditableButtonItems( + clipboardStatus: _clipboardStatus.value, + onLiveTextInput: null, + onCopy: copyEnabled + ? () => copySelection(SelectionChangedCause.toolbar) + : null, + onCut: + cutEnabled ? () => cutSelection(SelectionChangedCause.toolbar) : null, + onPaste: + pasteEnabled ? () => pasteText(SelectionChangedCause.toolbar) : null, + onSelectAll: selectAllEnabled + ? () => selectAll(SelectionChangedCause.toolbar) + : null, + ); + } + + /// Returns the anchor points for the default context menu. + /// + /// Copied from [EditableTextState]. + TextSelectionToolbarAnchors get contextMenuAnchors { + final glyphHeights = _getGlyphHeights(); + final selection = textEditingValue.selection; + final points = renderEditor.getEndpointsForSelection(selection); + return TextSelectionToolbarAnchors.fromSelection( + renderBox: renderEditor, + startGlyphHeight: glyphHeights.startGlyphHeight, + endGlyphHeight: glyphHeights.endGlyphHeight, + selectionEndpoints: points, + ); + } + + /// Gets the line heights at the start and end of the selection for the given + /// [QuillRawEditorState]. + /// + /// Copied from [EditableTextState]. + QuillEditorGlyphHeights _getGlyphHeights() { + final selection = textEditingValue.selection; + + // Only calculate handle rects if the text in the previous frame + // is the same as the text in the current frame. This is done because + // widget.renderObject contains the renderEditable from the previous frame. + // If the text changed between the current and previous frames then + // widget.renderObject.getRectForComposingRange might fail. In cases where + // the current frame is different from the previous we fall back to + // renderObject.preferredLineHeight. + final prevText = renderEditor.document.toPlainText(); + final currText = textEditingValue.text; + if (prevText != currText || !selection.isValid || selection.isCollapsed) { + return QuillEditorGlyphHeights( + renderEditor.preferredLineHeight(selection.base), + renderEditor.preferredLineHeight(selection.base), + ); + } + + final startCharacterRect = + renderEditor.getLocalRectForCaret(selection.base); + final endCharacterRect = + renderEditor.getLocalRectForCaret(selection.extent); + return QuillEditorGlyphHeights( + startCharacterRect.height, + endCharacterRect.height, + ); + } + + void _defaultOnTapOutside(PointerDownEvent event) { + /// The focus dropping behavior is only present on desktop platforms + /// and mobile browsers. + switch (defaultTargetPlatform) { + case TargetPlatform.android: + case TargetPlatform.iOS: + case TargetPlatform.fuchsia: + // On mobile platforms, we don't unfocus on touch events unless they're + // in the web browser, but we do unfocus for all other kinds of events. + switch (event.kind) { + case ui.PointerDeviceKind.touch: + break; + case ui.PointerDeviceKind.mouse: + case ui.PointerDeviceKind.stylus: + case ui.PointerDeviceKind.invertedStylus: + case ui.PointerDeviceKind.unknown: + widget.configurations.focusNode.unfocus(); + break; + case ui.PointerDeviceKind.trackpad: + throw UnimplementedError( + 'Unexpected pointer down event for trackpad.', + ); + } + break; + case TargetPlatform.linux: + case TargetPlatform.macOS: + case TargetPlatform.windows: + widget.configurations.focusNode.unfocus(); + break; + default: + throw UnsupportedError( + 'The platform ${defaultTargetPlatform.name} is not supported in the' + ' _defaultOnTapOutside()', + ); + } + } + + @override + Widget build(BuildContext context) { + assert(debugCheckHasMediaQuery(context)); + super.build(context); + + var doc = controller.document; + if (doc.isEmpty() && widget.configurations.placeholder != null) { + final raw = widget.configurations.placeholder?.replaceAll(r'"', '\\"'); + doc = Document.fromJson( + jsonDecode( + '[{"attributes":{"placeholder":true},"insert":"$raw\\n"}]', + ), + ); + } + + Widget child = CompositedTransformTarget( + link: _toolbarLayerLink, + child: Semantics( + child: MouseRegion( + cursor: SystemMouseCursors.text, + child: QuilRawEditorMultiChildRenderObject( + key: _editorKey, + document: doc, + selection: controller.selection, + hasFocus: _hasFocus, + scrollable: widget.configurations.scrollable, + cursorController: _cursorCont, + textDirection: _textDirection, + startHandleLayerLink: _startHandleLayerLink, + endHandleLayerLink: _endHandleLayerLink, + onSelectionChanged: _handleSelectionChanged, + onSelectionCompleted: _handleSelectionCompleted, + scrollBottomInset: widget.configurations.scrollBottomInset, + padding: widget.configurations.padding, + maxContentWidth: widget.configurations.maxContentWidth, + floatingCursorDisabled: + widget.configurations.floatingCursorDisabled, + children: _buildChildren(doc, context), + ), + ), + ), + ); + + if (widget.configurations.scrollable) { + /// Since [SingleChildScrollView] does not implement + /// `computeDistanceToActualBaseline` it prevents the editor from + /// providing its baseline metrics. To address this issue we wrap + /// the scroll view with [BaselineProxy] which mimics the editor's + /// baseline. + // This implies that the first line has no styles applied to it. + final baselinePadding = + EdgeInsets.only(top: _styles!.paragraph!.verticalSpacing.top); + child = BaselineProxy( + textStyle: _styles!.paragraph!.style, + padding: baselinePadding, + child: QuillSingleChildScrollView( + controller: _scrollController, + physics: widget.configurations.scrollPhysics, + viewportBuilder: (_, offset) => CompositedTransformTarget( + link: _toolbarLayerLink, + child: MouseRegion( + cursor: SystemMouseCursors.text, + child: QuilRawEditorMultiChildRenderObject( + key: _editorKey, + offset: offset, + document: doc, + selection: controller.selection, + hasFocus: _hasFocus, + scrollable: widget.configurations.scrollable, + textDirection: _textDirection, + startHandleLayerLink: _startHandleLayerLink, + endHandleLayerLink: _endHandleLayerLink, + onSelectionChanged: _handleSelectionChanged, + onSelectionCompleted: _handleSelectionCompleted, + scrollBottomInset: widget.configurations.scrollBottomInset, + padding: widget.configurations.padding, + maxContentWidth: widget.configurations.maxContentWidth, + cursorController: _cursorCont, + floatingCursorDisabled: + widget.configurations.floatingCursorDisabled, + children: _buildChildren(doc, context), + ), + ), + ), + ), + ); + } + + final constraints = widget.configurations.expands + ? const BoxConstraints.expand() + : BoxConstraints( + minHeight: widget.configurations.minHeight ?? 0.0, + maxHeight: widget.configurations.maxHeight ?? double.infinity, + ); + + // Please notice that this change will make the check fixed + // so if we ovveride the platform in material app theme data + // it will not depend on it and doesn't change here but I don't think + // we need to + final isDesktopMacOS = isMacOS(supportWeb: true); + + return TextFieldTapRegion( + enabled: widget.configurations.isOnTapOutsideEnabled, + onTapOutside: (event) { + final onTapOutside = widget.configurations.onTapOutside; + if (onTapOutside != null) { + onTapOutside.call(event, widget.configurations.focusNode); + return; + } + _defaultOnTapOutside(event); + }, + child: QuillStyles( + data: _styles!, + child: Shortcuts( + shortcuts: mergeMaps({ + // shortcuts added for Desktop platforms. + const SingleActivator( + LogicalKeyboardKey.escape, + ): const HideSelectionToolbarIntent(), + SingleActivator( + LogicalKeyboardKey.keyZ, + control: !isDesktopMacOS, + meta: isDesktopMacOS, + ): const UndoTextIntent(SelectionChangedCause.keyboard), + SingleActivator( + LogicalKeyboardKey.keyY, + control: !isDesktopMacOS, + meta: isDesktopMacOS, + ): const RedoTextIntent(SelectionChangedCause.keyboard), + + // Selection formatting. + SingleActivator( + LogicalKeyboardKey.keyB, + control: !isDesktopMacOS, + meta: isDesktopMacOS, + ): const ToggleTextStyleIntent(Attribute.bold), + SingleActivator( + LogicalKeyboardKey.keyU, + control: !isDesktopMacOS, + meta: isDesktopMacOS, + ): const ToggleTextStyleIntent(Attribute.underline), + SingleActivator( + LogicalKeyboardKey.keyI, + control: !isDesktopMacOS, + meta: isDesktopMacOS, + ): const ToggleTextStyleIntent(Attribute.italic), + SingleActivator( + LogicalKeyboardKey.keyS, + control: !isDesktopMacOS, + meta: isDesktopMacOS, + shift: true, + ): const ToggleTextStyleIntent(Attribute.strikeThrough), + SingleActivator( + LogicalKeyboardKey.backquote, + control: !isDesktopMacOS, + meta: isDesktopMacOS, + ): const ToggleTextStyleIntent(Attribute.inlineCode), + SingleActivator( + LogicalKeyboardKey.tilde, + control: !isDesktopMacOS, + meta: isDesktopMacOS, + shift: true, + ): const ToggleTextStyleIntent(Attribute.codeBlock), + SingleActivator( + LogicalKeyboardKey.keyB, + control: !isDesktopMacOS, + meta: isDesktopMacOS, + shift: true, + ): const ToggleTextStyleIntent(Attribute.blockQuote), + SingleActivator( + LogicalKeyboardKey.keyK, + control: !isDesktopMacOS, + meta: isDesktopMacOS, + ): const QuillEditorApplyLinkIntent(), + + // Lists + SingleActivator( + LogicalKeyboardKey.keyL, + control: !isDesktopMacOS, + meta: isDesktopMacOS, + shift: true, + ): const ToggleTextStyleIntent(Attribute.ul), + SingleActivator( + LogicalKeyboardKey.keyO, + control: !isDesktopMacOS, + meta: isDesktopMacOS, + shift: true, + ): const ToggleTextStyleIntent(Attribute.ol), + SingleActivator( + LogicalKeyboardKey.keyC, + control: !isDesktopMacOS, + meta: isDesktopMacOS, + shift: true, + ): const QuillEditorApplyCheckListIntent(), + + // Indents + SingleActivator( + LogicalKeyboardKey.keyM, + control: !isDesktopMacOS, + meta: isDesktopMacOS, + ): const IndentSelectionIntent(true), + SingleActivator( + LogicalKeyboardKey.keyM, + control: !isDesktopMacOS, + meta: isDesktopMacOS, + shift: true, + ): const IndentSelectionIntent(false), + + // Headers + SingleActivator( + LogicalKeyboardKey.digit1, + control: !isDesktopMacOS, + meta: isDesktopMacOS, + ): const QuillEditorApplyHeaderIntent(Attribute.h1), + SingleActivator( + LogicalKeyboardKey.digit2, + control: !isDesktopMacOS, + meta: isDesktopMacOS, + ): const QuillEditorApplyHeaderIntent(Attribute.h2), + SingleActivator( + LogicalKeyboardKey.digit3, + control: !isDesktopMacOS, + meta: isDesktopMacOS, + ): const QuillEditorApplyHeaderIntent(Attribute.h3), + SingleActivator( + LogicalKeyboardKey.digit0, + control: !isDesktopMacOS, + meta: isDesktopMacOS, + ): const QuillEditorApplyHeaderIntent(Attribute.header), + + SingleActivator( + LogicalKeyboardKey.keyG, + control: !isDesktopMacOS, + meta: isDesktopMacOS, + ): const QuillEditorInsertEmbedIntent(Attribute.image), + + SingleActivator( + LogicalKeyboardKey.keyF, + control: !isDesktopMacOS, + meta: isDesktopMacOS, + ): const OpenSearchIntent(), + }, { + ...?widget.configurations.customShortcuts + }), + child: Actions( + actions: mergeMaps>(_actions, { + ...?widget.configurations.customActions, + }), + child: Focus( + focusNode: widget.configurations.focusNode, + onKey: _onKey, + child: QuillKeyboardListener( + child: Container( + constraints: constraints, + child: child, + ), + ), + ), + ), + ), + ), + ); + } + + KeyEventResult _onKey(node, RawKeyEvent event) { + // Don't handle key if there is a meta key pressed. + if (event.isAltPressed || event.isControlPressed || event.isMetaPressed) { + return KeyEventResult.ignored; + } + + if (event is! RawKeyDownEvent) { + return KeyEventResult.ignored; + } + // Handle indenting blocks when pressing the tab key. + if (event.logicalKey == LogicalKeyboardKey.tab) { + return _handleTabKey(event); + } + + // Don't handle key if there is an active selection. + if (controller.selection.baseOffset != controller.selection.extentOffset) { + return KeyEventResult.ignored; + } + + // Handle inserting lists when space is pressed following + // a list initiating phrase. + if (event.logicalKey == LogicalKeyboardKey.space) { + return _handleSpaceKey(event); + } + + return KeyEventResult.ignored; + } + + KeyEventResult _handleSpaceKey(RawKeyEvent event) { + final child = + controller.document.queryChild(controller.selection.baseOffset); + if (child.node == null) { + return KeyEventResult.ignored; + } + + final line = child.node as Line?; + if (line == null) { + return KeyEventResult.ignored; + } + + final text = castOrNull(line.first); + if (text == null) { + return KeyEventResult.ignored; + } + + const olKeyPhrase = '1.'; + const ulKeyPhrase = '-'; + + if (text.value == olKeyPhrase) { + _updateSelectionForKeyPhrase(olKeyPhrase, Attribute.ol); + } else if (text.value == ulKeyPhrase) { + _updateSelectionForKeyPhrase(ulKeyPhrase, Attribute.ul); + } else { + return KeyEventResult.ignored; + } + + return KeyEventResult.handled; + } + + KeyEventResult _handleTabKey(RawKeyEvent event) { + final child = + controller.document.queryChild(controller.selection.baseOffset); + + KeyEventResult insertTabCharacter() { + if (widget.configurations.isReadOnly) { + return KeyEventResult.ignored; + } + controller.replaceText(controller.selection.baseOffset, 0, '\t', null); + _moveCursor(1); + return KeyEventResult.handled; + } + + if (controller.selection.baseOffset != controller.selection.extentOffset) { + if (child.node == null || child.node!.parent == null) { + return KeyEventResult.handled; + } + final parentBlock = child.node!.parent!; + if (parentBlock.style.containsKey(Attribute.ol.key) || + parentBlock.style.containsKey(Attribute.ul.key) || + parentBlock.style.containsKey(Attribute.checked.key)) { + controller.indentSelection(!event.isShiftPressed); + } + return KeyEventResult.handled; + } + + if (child.node == null) { + return insertTabCharacter(); + } + + final node = child.node!; + + final parent = node.parent; + if (parent == null || parent is! Block) { + return insertTabCharacter(); + } + + if (node is! Line || (node.isNotEmpty && node.first is! leaf.QuillText)) { + return insertTabCharacter(); + } + + final parentBlock = parent; + if (parentBlock.style.containsKey(Attribute.ol.key) || + parentBlock.style.containsKey(Attribute.ul.key) || + parentBlock.style.containsKey(Attribute.checked.key)) { + if (node.isNotEmpty && + (node.first as leaf.QuillText).value.isNotEmpty && + controller.selection.base.offset > node.documentOffset) { + return insertTabCharacter(); + } + controller.indentSelection(!event.isShiftPressed); + return KeyEventResult.handled; + } + + if (node.isNotEmpty && (node.first as leaf.QuillText).value.isNotEmpty) { + return insertTabCharacter(); + } + + return insertTabCharacter(); + } + + void _moveCursor(int chars) { + final selection = controller.selection; + controller.updateSelection( + controller.selection.copyWith( + baseOffset: selection.baseOffset + chars, + extentOffset: selection.baseOffset + chars), + ChangeSource.local); + } + + void _updateSelectionForKeyPhrase(String phrase, Attribute attribute) { + controller.replaceText(controller.selection.baseOffset - phrase.length, + phrase.length, '\n', null); + _moveCursor(-phrase.length); + controller + ..formatSelection(attribute) + // Remove the added newline. + ..replaceText(controller.selection.baseOffset + 1, 1, '', null); + } + + void _handleSelectionChanged( + TextSelection selection, + SelectionChangedCause cause, + ) { + final oldSelection = controller.selection; + controller.updateSelection(selection, ChangeSource.local); + + _selectionOverlay?.handlesVisible = _shouldShowSelectionHandles(); + + if (!_keyboardVisible) { + // This will show the keyboard for all selection changes on the + // editor, not just changes triggered by user gestures. + requestKeyboard(); + } + + if (cause == SelectionChangedCause.drag) { + // When user updates the selection while dragging make sure to + // bring the updated position (base or extent) into view. + if (oldSelection.baseOffset != selection.baseOffset) { + bringIntoView(selection.base); + } else if (oldSelection.extentOffset != selection.extentOffset) { + bringIntoView(selection.extent); + } + } + } + + void _handleSelectionCompleted() { + controller.onSelectionCompleted?.call(); + } + + /// Updates the checkbox positioned at [offset] in document + /// by changing its attribute according to [value]. + void _handleCheckboxTap(int offset, bool value) { + final requestKeyboardFocusOnCheckListChanged = + widget.configurations.requestKeyboardFocusOnCheckListChanged; + if (!widget.configurations.isReadOnly) { + _disableScrollControllerAnimateOnce = true; + final currentSelection = controller.selection.copyWith(); + final attribute = value ? Attribute.checked : Attribute.unchecked; + + _markNeedsBuild(); + controller + ..ignoreFocusOnTextChange = true + ..skipRequestKeyboard = !requestKeyboardFocusOnCheckListChanged + ..formatText(offset, 0, attribute) + + // Checkbox tapping causes controller.selection to go to offset 0 + // Stop toggling those two toolbar buttons + ..toolbarButtonToggler = { + Attribute.list.key: attribute, + Attribute.header.key: Attribute.header + }; + + // Go back from offset 0 to current selection + SchedulerBinding.instance.addPostFrameCallback((_) { + controller + ..ignoreFocusOnTextChange = false + ..skipRequestKeyboard = !requestKeyboardFocusOnCheckListChanged + ..updateSelection(currentSelection, ChangeSource.local); + }); + } + } + + List _buildChildren(Document doc, BuildContext context) { + final result = []; + final indentLevelCounts = {}; + // this need for several ordered list in document + // we need to reset indents Map, if list finished + // List finished when there is node without Attribute.ol in styles + // So in this case we set clearIndents=true and send it + // to the next EditableTextBlock + var prevNodeOl = false; + var clearIndents = false; + + for (final node in doc.root.children) { + final attrs = node.style.attributes; + + if (prevNodeOl && attrs[Attribute.list.key] != Attribute.ol) { + clearIndents = true; + } + + prevNodeOl = attrs[Attribute.list.key] == Attribute.ol; + + if (node is Line) { + final editableTextLine = _getEditableTextLineFromNode(node, context); + result.add(Directionality( + textDirection: getDirectionOfNode(node), child: editableTextLine)); + } else if (node is Block) { + final editableTextBlock = EditableTextBlock( + block: node, + controller: controller, + textDirection: getDirectionOfNode(node), + scrollBottomInset: widget.configurations.scrollBottomInset, + verticalSpacing: _getVerticalSpacingForBlock(node, _styles), + textSelection: controller.selection, + color: widget.configurations.selectionColor, + styles: _styles, + enableInteractiveSelection: + widget.configurations.enableInteractiveSelection, + hasFocus: _hasFocus, + contentPadding: attrs.containsKey(Attribute.codeBlock.key) + ? const EdgeInsets.all(16) + : null, + embedBuilder: widget.configurations.embedBuilder, + linkActionPicker: _linkActionPicker, + onLaunchUrl: widget.configurations.onLaunchUrl, + cursorCont: _cursorCont, + indentLevelCounts: indentLevelCounts, + clearIndents: clearIndents, + onCheckboxTap: _handleCheckboxTap, + readOnly: widget.configurations.isReadOnly, + customStyleBuilder: widget.configurations.customStyleBuilder, + customLinkPrefixes: widget.configurations.customLinkPrefixes, + ); + result.add( + Directionality( + textDirection: getDirectionOfNode(node), + child: editableTextBlock, + ), + ); + + clearIndents = false; + } else { + _dirty = false; + throw StateError('Unreachable.'); + } + } + _dirty = false; + return result; + } + + EditableTextLine _getEditableTextLineFromNode( + Line node, BuildContext context) { + final textLine = TextLine( + line: node, + textDirection: _textDirection, + embedBuilder: widget.configurations.embedBuilder, + customStyleBuilder: widget.configurations.customStyleBuilder, + customRecognizerBuilder: widget.configurations.customRecognizerBuilder, + styles: _styles!, + readOnly: widget.configurations.isReadOnly, + controller: controller, + linkActionPicker: _linkActionPicker, + onLaunchUrl: widget.configurations.onLaunchUrl, + customLinkPrefixes: widget.configurations.customLinkPrefixes, + ); + final editableTextLine = EditableTextLine( + node, + null, + textLine, + 0, + _getVerticalSpacingForLine(node, _styles), + _textDirection, + controller.selection, + widget.configurations.selectionColor, + widget.configurations.enableInteractiveSelection, + _hasFocus, + MediaQuery.devicePixelRatioOf(context), + _cursorCont); + return editableTextLine; + } + + VerticalSpacing _getVerticalSpacingForLine( + Line line, + DefaultStyles? defaultStyles, + ) { + final attrs = line.style.attributes; + if (attrs.containsKey(Attribute.header.key)) { + int level; + if (attrs[Attribute.header.key]!.value is double) { + level = attrs[Attribute.header.key]!.value.toInt(); + } else { + level = attrs[Attribute.header.key]!.value; + } + switch (level) { + case 1: + return defaultStyles!.h1!.verticalSpacing; + case 2: + return defaultStyles!.h2!.verticalSpacing; + case 3: + return defaultStyles!.h3!.verticalSpacing; + default: + throw ArgumentError('Invalid level $level'); + } + } + + return defaultStyles!.paragraph!.verticalSpacing; + } + + VerticalSpacing _getVerticalSpacingForBlock( + Block node, DefaultStyles? defaultStyles) { + final attrs = node.style.attributes; + if (attrs.containsKey(Attribute.blockQuote.key)) { + return defaultStyles!.quote!.verticalSpacing; + } else if (attrs.containsKey(Attribute.codeBlock.key)) { + return defaultStyles!.code!.verticalSpacing; + } else if (attrs.containsKey(Attribute.indent.key)) { + return defaultStyles!.indent!.verticalSpacing; + } else if (attrs.containsKey(Attribute.list.key)) { + return defaultStyles!.lists!.verticalSpacing; + } else if (attrs.containsKey(Attribute.align.key)) { + return defaultStyles!.align!.verticalSpacing; + } + return const VerticalSpacing(0, 0); + } + + void _didChangeTextEditingValueListener() { + _didChangeTextEditingValue(controller.ignoreFocusOnTextChange); + } + + @override + void initState() { + super.initState(); + + _clipboardStatus.addListener(_onChangedClipboardStatus); + + controller.addListener(_didChangeTextEditingValueListener); + + _scrollController = widget.configurations.scrollController; + _scrollController.addListener(_updateSelectionOverlayForScroll); + + _cursorCont = CursorCont( + show: ValueNotifier(widget.configurations.showCursor), + style: widget.configurations.cursorStyle, + tickerProvider: this, + ); + + // Floating cursor + _floatingCursorResetController = AnimationController(vsync: this); + _floatingCursorResetController.addListener(onFloatingCursorResetTick); + + if (isKeyboardOS(supportWeb: true)) { + _keyboardVisible = true; + } else if (!isWeb() && isFlutterTest()) { + // treat tests like a keyboard OS + _keyboardVisible = true; + } else { + // treat iOS Simulator like a keyboard OS + isIOSSimulator().then((isIosSimulator) { + if (isIosSimulator) { + _keyboardVisible = true; + } else { + _keyboardVisibilityController = KeyboardVisibilityController(); + _keyboardVisible = _keyboardVisibilityController!.isVisible; + _keyboardVisibilitySubscription = + _keyboardVisibilityController?.onChange.listen((visible) { + _keyboardVisible = visible; + if (visible) { + _onChangeTextEditingValue(!_hasFocus); + } + }); + + HardwareKeyboard.instance.addHandler(_hardwareKeyboardEvent); + } + }); + } + + // Focus + widget.configurations.focusNode.addListener(_handleFocusChanged); + } + + // KeyboardVisibilityController only checks for keyboards that + // adjust the screen size. Also watch for hardware keyboards + // that don't alter the screen (i.e. Chromebook, Android tablet + // and any hardware keyboards from an OS not listed in isKeyboardOS()) + bool _hardwareKeyboardEvent(KeyEvent _) { + if (!_keyboardVisible) { + // hardware keyboard key pressed. Set visibility to true + _keyboardVisible = true; + // update the editor + _onChangeTextEditingValue(!_hasFocus); + } + + // remove the key handler - it's no longer needed. If + // KeyboardVisibilityController clears visibility, it wil + // also enable it when appropriate. + HardwareKeyboard.instance.removeHandler(_hardwareKeyboardEvent); + + // we didn't handle the event, just needed to know a key was pressed + return false; + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + final parentStyles = QuillStyles.getStyles(context, true); + final defaultStyles = DefaultStyles.getInstance(context); + _styles = (parentStyles != null) + ? defaultStyles.merge(parentStyles) + : defaultStyles; + + if (widget.configurations.customStyles != null) { + _styles = _styles!.merge(widget.configurations.customStyles!); + } + + _requestAutoFocusIfShould(); + } + + Future _requestAutoFocusIfShould() async { + final focusManager = FocusScope.of(context); + if (!_didAutoFocus && widget.configurations.autoFocus) { + await Future.delayed(Duration.zero); // To avoid exceptions + focusManager.autofocus(widget.configurations.focusNode); + _didAutoFocus = true; + } + } + + @override + void didUpdateWidget(QuillRawEditor oldWidget) { + super.didUpdateWidget(oldWidget); + + _cursorCont.show.value = widget.configurations.showCursor; + _cursorCont.style = widget.configurations.cursorStyle; + + if (controller != oldWidget.configurations.controller) { + oldWidget.configurations.controller + .removeListener(_didChangeTextEditingValue); + controller.addListener(_didChangeTextEditingValue); + updateRemoteValueIfNeeded(); + } + + if (widget.configurations.scrollController != _scrollController) { + _scrollController.removeListener(_updateSelectionOverlayForScroll); + _scrollController = widget.configurations.scrollController; + _scrollController.addListener(_updateSelectionOverlayForScroll); + } + + if (widget.configurations.focusNode != oldWidget.configurations.focusNode) { + oldWidget.configurations.focusNode.removeListener(_handleFocusChanged); + widget.configurations.focusNode.addListener(_handleFocusChanged); + updateKeepAlive(); + } + + if (controller.selection != oldWidget.configurations.controller.selection) { + _selectionOverlay?.update(textEditingValue); + } + + _selectionOverlay?.handlesVisible = _shouldShowSelectionHandles(); + if (!shouldCreateInputConnection) { + closeConnectionIfNeeded(); + } else { + if (oldWidget.configurations.isReadOnly && _hasFocus) { + openConnectionIfNeeded(); + } + } + + // in case customStyles changed in new widget + if (widget.configurations.customStyles != null) { + _styles = _styles!.merge(widget.configurations.customStyles!); + } + } + + bool _shouldShowSelectionHandles() { + return widget.configurations.showSelectionHandles && + !controller.selection.isCollapsed; + } + + @override + void dispose() { + closeConnectionIfNeeded(); + _keyboardVisibilitySubscription?.cancel(); + HardwareKeyboard.instance.removeHandler(_hardwareKeyboardEvent); + assert(!hasConnection); + _selectionOverlay?.dispose(); + _selectionOverlay = null; + controller.removeListener(_didChangeTextEditingValueListener); + widget.configurations.focusNode.removeListener(_handleFocusChanged); + _cursorCont.dispose(); + _clipboardStatus + ..removeListener(_onChangedClipboardStatus) + ..dispose(); + super.dispose(); + } + + void _updateSelectionOverlayForScroll() { + _selectionOverlay?.updateForScroll(); + } + + /// Marks the editor as dirty and trigger a rebuild. + /// + /// When the editor is dirty methods that depend on the editor + /// state being in sync with the controller know they may be + /// operating on stale data. + void _markNeedsBuild() { + if (_dirty) { + // No need to rebuilt if it already darty + return; + } + setState(() { + _dirty = true; + }); + } + + void _didChangeTextEditingValue([bool ignoreFocus = false]) { + if (isWeb()) { + _onChangeTextEditingValue(ignoreFocus); + if (!ignoreFocus) { + requestKeyboard(); + } + return; + } + + if (ignoreFocus || _keyboardVisible) { + _onChangeTextEditingValue(ignoreFocus); + } else { + requestKeyboard(); + if (mounted) { + // Use controller.value in build() + // Mark widget as dirty and trigger build and updateChildren + _markNeedsBuild(); + } + } + + _adjacentLineAction.stopCurrentVerticalRunIfSelectionChanges(); + } + + void _onChangeTextEditingValue([bool ignoreCaret = false]) { + updateRemoteValueIfNeeded(); + if (ignoreCaret) { + return; + } + _showCaretOnScreen(); + _cursorCont.startOrStopCursorTimerIfNeeded(_hasFocus, controller.selection); + if (hasConnection) { + // To keep the cursor from blinking while typing, we want to restart the + // cursor timer every time a new character is typed. + _cursorCont + ..stopCursorTimer(resetCharTicks: false) + ..startCursorTimer(); + } + + // Refresh selection overlay after the build step had a chance to + // update and register all children of RenderEditor. Otherwise this will + // fail in situations where a new line of text is entered, which adds + // a new RenderEditableBox child. If we try to update selection overlay + // immediately it'll not be able to find the new child since it hasn't been + // built yet. + SchedulerBinding.instance.addPostFrameCallback((_) { + if (!mounted) { + return; + } + _updateOrDisposeSelectionOverlayIfNeeded(); + }); + if (mounted) { + // Use controller.value in build() + // Mark widget as dirty and trigger build and updateChildren + _markNeedsBuild(); + } + } + + void _updateOrDisposeSelectionOverlayIfNeeded() { + if (_selectionOverlay != null) { + if (!_hasFocus || textEditingValue.selection.isCollapsed) { + _selectionOverlay!.dispose(); + _selectionOverlay = null; + } else { + _selectionOverlay!.update(textEditingValue); + } + } else if (_hasFocus) { + _selectionOverlay = EditorTextSelectionOverlay( + value: textEditingValue, + context: context, + debugRequiredFor: widget, + startHandleLayerLink: _startHandleLayerLink, + endHandleLayerLink: _endHandleLayerLink, + renderObject: renderEditor, + selectionCtrls: widget.configurations.selectionCtrls, + selectionDelegate: this, + clipboardStatus: _clipboardStatus, + contextMenuBuilder: widget.configurations.contextMenuBuilder == null + ? null + : (context) => + widget.configurations.contextMenuBuilder!(context, this), + ); + _selectionOverlay!.handlesVisible = _shouldShowSelectionHandles(); + _selectionOverlay!.showHandles(); + } + } + + void _handleFocusChanged() { + if (dirty) { + SchedulerBinding.instance + .addPostFrameCallback((_) => _handleFocusChanged()); + return; + } + openOrCloseConnection(); + _cursorCont.startOrStopCursorTimerIfNeeded(_hasFocus, controller.selection); + _updateOrDisposeSelectionOverlayIfNeeded(); + if (_hasFocus) { + WidgetsBinding.instance.addObserver(this); + _showCaretOnScreen(); + } else { + WidgetsBinding.instance.removeObserver(this); + } + updateKeepAlive(); + } + + void _onChangedClipboardStatus() { + if (!mounted) return; + // Inform the widget that the value of clipboardStatus has changed. + // Trigger build and updateChildren + _markNeedsBuild(); + } + + Future _linkActionPicker(Node linkNode) async { + final link = linkNode.style.attributes[Attribute.link.key]!.value!; + return widget.configurations + .linkActionPickerDelegate(context, link, linkNode); + } + + bool _showCaretOnScreenScheduled = false; + + // This is a workaround for checkbox tapping issue + // https://github.com/singerdmx/flutter-quill/issues/619 + // We cannot treat {"list": "checked"} and {"list": "unchecked"} as + // block of the same style + // This causes controller.selection to go to offset 0 + bool _disableScrollControllerAnimateOnce = false; + + void _showCaretOnScreen() { + if (!widget.configurations.showCursor || _showCaretOnScreenScheduled) { + return; + } + + _showCaretOnScreenScheduled = true; + SchedulerBinding.instance.addPostFrameCallback((_) { + if (widget.configurations.scrollable || _scrollController.hasClients) { + _showCaretOnScreenScheduled = false; + + if (!mounted) { + return; + } + + final viewport = RenderAbstractViewport.of(renderEditor); + final editorOffset = + renderEditor.localToGlobal(const Offset(0, 0), ancestor: viewport); + final offsetInViewport = _scrollController.offset + editorOffset.dy; + + final offset = renderEditor.getOffsetToRevealCursor( + _scrollController.position.viewportDimension, + _scrollController.offset, + offsetInViewport, + ); + + if (offset != null) { + if (_disableScrollControllerAnimateOnce) { + _disableScrollControllerAnimateOnce = false; + return; + } + _scrollController.animateTo( + math.min(offset, _scrollController.position.maxScrollExtent), + duration: const Duration(milliseconds: 100), + curve: Curves.fastOutSlowIn, + ); + } + } + }); + } + + /// The renderer for this widget's editor descendant. + /// + /// This property is typically used to notify the renderer of input gestures. + @override + RenderEditor get renderEditor => + _editorKey.currentContext!.findRenderObject() as RenderEditor; + + /// Express interest in interacting with the keyboard. + /// + /// If this control is already attached to the keyboard, this function will + /// request that the keyboard become visible. Otherwise, this function will + /// ask the focus system that it become focused. If successful in acquiring + /// focus, the control will then attach to the keyboard and request that the + /// keyboard become visible. + @override + void requestKeyboard() { + if (controller.skipRequestKeyboard) { + // and that just by one simple change + controller.skipRequestKeyboard = false; + return; + } + if (_hasFocus) { + final keyboardAlreadyShown = _keyboardVisible; + openConnectionIfNeeded(); + if (!keyboardAlreadyShown) { + /// delay 500 milliseconds for waiting keyboard show up + Future.delayed( + const Duration(milliseconds: 500), + _showCaretOnScreen, + ); + } else { + _showCaretOnScreen(); + } + } else { + widget.configurations.focusNode.requestFocus(); + } + } + + /// Shows the selection toolbar at the location of the current cursor. + /// + /// Returns `false` if a toolbar couldn't be shown, such as when the toolbar + /// is already shown, or when no text selection currently exists. + @override + bool showToolbar() { + // Web is using native dom elements to enable clipboard functionality of the + // toolbar: copy, paste, select, cut. It might also provide additional + // functionality depending on the browser (such as translate). Due to this + // we should not show a Flutter toolbar for the editable text elements. + if (isWeb()) { + return false; + } + + // selectionOverlay is aggressively released when selection is collapsed + // to remove unnecessary handles. Since a toolbar is requested here, + // attempt to create the selectionOverlay if it's not already created. + if (_selectionOverlay == null) { + _updateOrDisposeSelectionOverlayIfNeeded(); + } + + if (_selectionOverlay == null || _selectionOverlay!.toolbar != null) { + return false; + } + + _selectionOverlay!.update(textEditingValue); + _selectionOverlay!.showToolbar(); + return true; + } + + void _replaceText(ReplaceTextIntent intent) { + userUpdateTextEditingValue( + intent.currentTextEditingValue + .replaced(intent.replacementRange, intent.replacementText), + intent.cause, + ); + } + + /// Copy current selection to [Clipboard]. + @override + void copySelection(SelectionChangedCause cause) { + controller.copiedImageUrl = null; + _pastePlainText = controller.getPlainText(); + _pasteStyleAndEmbed = controller.getAllIndividualSelectionStylesAndEmbed(); + + final selection = textEditingValue.selection; + final text = textEditingValue.text; + if (selection.isCollapsed) { + return; + } + Clipboard.setData(ClipboardData(text: selection.textInside(text))); + + if (cause == SelectionChangedCause.toolbar) { + bringIntoView(textEditingValue.selection.extent); + + // Collapse the selection and hide the toolbar and handles. + userUpdateTextEditingValue( + TextEditingValue( + text: textEditingValue.text, + selection: + TextSelection.collapsed(offset: textEditingValue.selection.end), + ), + SelectionChangedCause.toolbar, + ); + } + } + + /// Cut current selection to [Clipboard]. + @override + void cutSelection(SelectionChangedCause cause) { + controller.copiedImageUrl = null; + _pastePlainText = controller.getPlainText(); + _pasteStyleAndEmbed = controller.getAllIndividualSelectionStylesAndEmbed(); + + if (widget.configurations.isReadOnly) { + return; + } + final selection = textEditingValue.selection; + final text = textEditingValue.text; + if (selection.isCollapsed) { + return; + } + Clipboard.setData(ClipboardData(text: selection.textInside(text))); + _replaceText(ReplaceTextIntent(textEditingValue, '', selection, cause)); + + if (cause == SelectionChangedCause.toolbar) { + bringIntoView(textEditingValue.selection.extent); + hideToolbar(); + } + } + + /// Paste text from [Clipboard]. + @override + Future pasteText(SelectionChangedCause cause) async { + if (widget.configurations.isReadOnly) { + return; + } + + if (controller.copiedImageUrl != null) { + final index = textEditingValue.selection.baseOffset; + final length = textEditingValue.selection.extentOffset - index; + final copied = controller.copiedImageUrl!; + controller.replaceText( + index, + length, + BlockEmbed.image(copied.url), + null, + ); + if (copied.styleString.isNotEmpty) { + controller.formatText( + getEmbedNode(controller, index + 1).offset, + 1, + StyleAttribute(copied.styleString), + ); + } + controller.copiedImageUrl = null; + await Clipboard.setData( + const ClipboardData(text: ''), + ); + return; + } + + final selection = textEditingValue.selection; + if (!selection.isValid) { + return; + } + // Snapshot the input before using `await`. + // See https://github.com/flutter/flutter/issues/11427 + final text = await Clipboard.getData(Clipboard.kTextPlain); + if (text != null) { + _replaceText( + ReplaceTextIntent( + textEditingValue, + text.text!, + selection, + cause, + ), + ); + + bringIntoView(textEditingValue.selection.extent); + + // Collapse the selection and hide the toolbar and handles. + userUpdateTextEditingValue( + TextEditingValue( + text: textEditingValue.text, + selection: TextSelection.collapsed( + offset: textEditingValue.selection.end, + ), + ), + cause, + ); + + return; + } + + final onImagePaste = widget.configurations.onImagePaste; + if (onImagePaste != null) { + final image = await Pasteboard.image; + + if (image == null) { + return; + } + + final imageUrl = await onImagePaste(image); + if (imageUrl == null) { + return; + } + + controller.replaceText( + textEditingValue.selection.end, + 0, + BlockEmbed.image(imageUrl), + null, + ); + } + } + + /// Select the entire text value. + @override + void selectAll(SelectionChangedCause cause) { + userUpdateTextEditingValue( + textEditingValue.copyWith( + selection: TextSelection( + baseOffset: 0, extentOffset: textEditingValue.text.length), + ), + cause, + ); + + if (cause == SelectionChangedCause.toolbar) { + bringIntoView(textEditingValue.selection.extent); + } + } + + @override + bool get wantKeepAlive => widget.configurations.focusNode.hasFocus; + + @override + AnimationController get floatingCursorResetController => + _floatingCursorResetController; + + late AnimationController _floatingCursorResetController; + + // --------------------------- Text Editing Actions -------------------------- + + QuillEditorTextBoundary _characterBoundary( + DirectionalTextEditingIntent intent) { + final atomicTextBoundary = QuillEditorCharacterBoundary(textEditingValue); + return QuillEditorCollapsedSelectionBoundary( + atomicTextBoundary, intent.forward); + } + + QuillEditorTextBoundary _nextWordBoundary( + DirectionalTextEditingIntent intent) { + final QuillEditorTextBoundary atomicTextBoundary; + final QuillEditorTextBoundary boundary; + + // final TextEditingValue textEditingValue = + // _textEditingValueforTextLayoutMetrics; + atomicTextBoundary = QuillEditorCharacterBoundary(textEditingValue); + // This isn't enough. Newline characters. + boundary = QuillEditorExpandedTextBoundary( + QuillEditorWhitespaceBoundary(textEditingValue), + QuillEditorWordBoundary(renderEditor, textEditingValue)); + + final mixedBoundary = intent.forward + ? QuillEditorMixedBoundary(atomicTextBoundary, boundary) + : QuillEditorMixedBoundary(boundary, atomicTextBoundary); + // Use a _MixedBoundary to make sure we don't leave invalid codepoints in + // the field after deletion. + return QuillEditorCollapsedSelectionBoundary(mixedBoundary, intent.forward); + } + + QuillEditorTextBoundary _linebreak(DirectionalTextEditingIntent intent) { + final QuillEditorTextBoundary atomicTextBoundary; + final QuillEditorTextBoundary boundary; + + // final TextEditingValue textEditingValue = + // _textEditingValueforTextLayoutMetrics; + atomicTextBoundary = QuillEditorCharacterBoundary(textEditingValue); + boundary = QuillEditorLineBreak(renderEditor, textEditingValue); + + // The _MixedBoundary is to make sure we don't leave invalid code units in + // the field after deletion. + // `boundary` doesn't need to be wrapped in a _CollapsedSelectionBoundary, + // since the document boundary is unique and the linebreak boundary is + // already caret-location based. + return intent.forward + ? QuillEditorMixedBoundary( + QuillEditorCollapsedSelectionBoundary(atomicTextBoundary, true), + boundary) + : QuillEditorMixedBoundary( + boundary, + QuillEditorCollapsedSelectionBoundary(atomicTextBoundary, false), + ); + } + + QuillEditorTextBoundary _documentBoundary( + DirectionalTextEditingIntent intent) => + QuillEditorDocumentBoundary(textEditingValue); + + Action _makeOverridable(Action defaultAction) { + return Action.overridable( + context: context, defaultAction: defaultAction); + } + + late final Action _replaceTextAction = + CallbackAction(onInvoke: _replaceText); + + void _updateSelection(UpdateSelectionIntent intent) { + userUpdateTextEditingValue( + intent.currentTextEditingValue.copyWith(selection: intent.newSelection), + intent.cause, + ); + } + + late final Action _updateSelectionAction = + CallbackAction(onInvoke: _updateSelection); + + late final QuillEditorUpdateTextSelectionToAdjacentLineAction< + ExtendSelectionVerticallyToAdjacentLineIntent> _adjacentLineAction = + QuillEditorUpdateTextSelectionToAdjacentLineAction< + ExtendSelectionVerticallyToAdjacentLineIntent>(this); + + late final QuillEditorToggleTextStyleAction _formatSelectionAction = + QuillEditorToggleTextStyleAction(this); + + late final QuillEditorIndentSelectionAction _indentSelectionAction = + QuillEditorIndentSelectionAction(this); + + late final QuillEditorOpenSearchAction _openSearchAction = + QuillEditorOpenSearchAction(this); + late final QuillEditorApplyHeaderAction _applyHeaderAction = + QuillEditorApplyHeaderAction(this); + late final QuillEditorApplyCheckListAction _applyCheckListAction = + QuillEditorApplyCheckListAction(this); + + late final Map> _actions = >{ + DoNothingAndStopPropagationTextIntent: DoNothingAction(consumesKey: false), + ReplaceTextIntent: _replaceTextAction, + UpdateSelectionIntent: _updateSelectionAction, + DirectionalFocusIntent: DirectionalFocusAction.forTextField(), + + // Delete + DeleteCharacterIntent: _makeOverridable( + QuillEditorDeleteTextAction( + this, _characterBoundary)), + DeleteToNextWordBoundaryIntent: _makeOverridable( + QuillEditorDeleteTextAction( + this, _nextWordBoundary)), + DeleteToLineBreakIntent: _makeOverridable( + QuillEditorDeleteTextAction(this, _linebreak)), + + // Extend/Move Selection + ExtendSelectionByCharacterIntent: _makeOverridable( + QuillEditorUpdateTextSelectionAction( + this, + false, + _characterBoundary, + )), + ExtendSelectionToNextWordBoundaryIntent: _makeOverridable( + QuillEditorUpdateTextSelectionAction< + ExtendSelectionToNextWordBoundaryIntent>( + this, true, _nextWordBoundary)), + ExtendSelectionToLineBreakIntent: _makeOverridable( + QuillEditorUpdateTextSelectionAction( + this, true, _linebreak)), + ExtendSelectionVerticallyToAdjacentLineIntent: + _makeOverridable(_adjacentLineAction), + ExtendSelectionToDocumentBoundaryIntent: _makeOverridable( + QuillEditorUpdateTextSelectionAction< + ExtendSelectionToDocumentBoundaryIntent>( + this, true, _documentBoundary)), + ExtendSelectionToNextWordBoundaryOrCaretLocationIntent: _makeOverridable( + QuillEditorExtendSelectionOrCaretPositionAction( + this, _nextWordBoundary)), + + // Copy Paste + SelectAllTextIntent: _makeOverridable(QuillEditorSelectAllAction(this)), + CopySelectionTextIntent: + _makeOverridable(QuillEditorCopySelectionAction(this)), + PasteTextIntent: _makeOverridable(CallbackAction( + onInvoke: (intent) => pasteText(intent.cause))), + + HideSelectionToolbarIntent: + _makeOverridable(QuillEditorHideSelectionToolbarAction(this)), + UndoTextIntent: _makeOverridable(QuillEditorUndoKeyboardAction(this)), + RedoTextIntent: _makeOverridable(QuillEditorRedoKeyboardAction(this)), + + OpenSearchIntent: _openSearchAction, + + // Selection Formatting + ToggleTextStyleIntent: _formatSelectionAction, + IndentSelectionIntent: _indentSelectionAction, + QuillEditorApplyHeaderIntent: _applyHeaderAction, + QuillEditorApplyCheckListIntent: _applyCheckListAction, + QuillEditorApplyLinkIntent: QuillEditorApplyLinkAction(this) + }; + + @override + void insertTextPlaceholder(Size size) { + // this is needed for Scribble (Stylus input) in Apple platforms + // and this package does not implement this feature + } + + @override + void removeTextPlaceholder() { + // this is needed for Scribble (Stylus input) in Apple platforms + // and this package does not implement this feature + } + + @override + void didChangeInputControl( + TextInputControl? oldControl, + TextInputControl? newControl, + ) { + // TODO: implement didChangeInputControl + } + + @override + void performSelector(String selectorName) { + final intent = intentForMacOSSelector(selectorName); + + if (intent != null) { + final primaryContext = primaryFocus?.context; + if (primaryContext != null) { + Actions.invoke(primaryContext, intent); + } + } + } + + @override + // TODO: implement liveTextInputEnabled + bool get liveTextInputEnabled => false; +} diff --git a/lib/src/widgets/raw_editor/raw_editor_state_selection_delegate_mixin.dart b/lib/src/widgets/raw_editor/raw_editor_state_selection_delegate_mixin.dart index 603402fa..46617573 100644 --- a/lib/src/widgets/raw_editor/raw_editor_state_selection_delegate_mixin.dart +++ b/lib/src/widgets/raw_editor/raw_editor_state_selection_delegate_mixin.dart @@ -14,17 +14,18 @@ mixin RawEditorStateSelectionDelegateMixin on EditorState implements TextSelectionDelegate { @override TextEditingValue get textEditingValue { - return widget.controller.plainTextEditingValue; + return widget.configurations.controller.plainTextEditingValue; } set textEditingValue(TextEditingValue value) { final cursorPosition = value.selection.extentOffset; - final oldText = widget.controller.document.toPlainText(); + final oldText = widget.configurations.controller.document.toPlainText(); final newText = value.text; final diff = getDiff(oldText, newText, cursorPosition); if (diff.deleted == '' && diff.inserted == '') { // Only changing selection range - widget.controller.updateSelection(value.selection, ChangeSource.LOCAL); + widget.configurations.controller + .updateSelection(value.selection, ChangeSource.local); return; } @@ -34,7 +35,7 @@ mixin RawEditorStateSelectionDelegateMixin on EditorState insertedText = containsEmbed ? _adjustInsertedText(diff.inserted) : diff.inserted; - widget.controller.replaceText( + widget.configurations.controller.replaceText( diff.start, diff.deleted.length, insertedText, value.selection); _applyPasteStyleAndEmbed(insertedText, diff.start, containsEmbed); @@ -51,19 +52,23 @@ mixin RawEditorStateSelectionDelegateMixin on EditorState final local = pos + offset; if (styleAndEmbed is Embeddable) { - widget.controller.replaceText(local, 0, styleAndEmbed, null); + widget.configurations.controller + .replaceText(local, 0, styleAndEmbed, null); } else { final style = styleAndEmbed as Style; if (style.isInline) { - widget.controller + widget.configurations.controller .formatTextStyle(local, pasteStyleAndEmbed[i].length!, style); } else if (style.isBlock) { - final node = widget.controller.document.queryChild(local).node; + final node = widget.configurations.controller.document + .queryChild(local) + .node; if (node != null && pasteStyleAndEmbed[i].length == node.length - 1) { - style.values.forEach((attribute) { - widget.controller.document.format(local, 0, attribute); - }); + for (final attribute in style.values) { + widget.configurations.controller.document + .format(local, 0, attribute); + } } } } @@ -164,15 +169,18 @@ mixin RawEditorStateSelectionDelegateMixin on EditorState } @override - bool get cutEnabled => widget.contextMenuBuilder != null && !widget.readOnly; + bool get cutEnabled => + widget.configurations.contextMenuBuilder != null && + !widget.configurations.isReadOnly; @override - bool get copyEnabled => widget.contextMenuBuilder != null; + bool get copyEnabled => widget.configurations.contextMenuBuilder != null; @override bool get pasteEnabled => - widget.contextMenuBuilder != null && !widget.readOnly; + widget.configurations.contextMenuBuilder != null && + !widget.configurations.isReadOnly; @override - bool get selectAllEnabled => widget.contextMenuBuilder != null; + bool get selectAllEnabled => widget.configurations.contextMenuBuilder != null; } diff --git a/lib/src/widgets/raw_editor/raw_editor_state_text_input_client_mixin.dart b/lib/src/widgets/raw_editor/raw_editor_state_text_input_client_mixin.dart index 2cd9ebf1..cc0ac14c 100644 --- a/lib/src/widgets/raw_editor/raw_editor_state_text_input_client_mixin.dart +++ b/lib/src/widgets/raw_editor/raw_editor_state_text_input_client_mixin.dart @@ -1,8 +1,8 @@ import 'dart:ui'; -import 'package:flutter/animation.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/scheduler.dart'; +import 'package:flutter/animation.dart' show Curves; +import 'package:flutter/foundation.dart' show kIsWeb; +import 'package:flutter/scheduler.dart' show SchedulerBinding; import 'package:flutter/services.dart'; import '../../models/documents/document.dart'; @@ -27,7 +27,8 @@ mixin RawEditorStateTextInputClientMixin on EditorState /// - cmd/ctrl+c shortcut to copy. /// - cmd/ctrl+a to select all. /// - Changing the selection using a physical keyboard. - bool get shouldCreateInputConnection => kIsWeb || !widget.readOnly; + bool get shouldCreateInputConnection => + kIsWeb || !widget.configurations.isReadOnly; /// Returns `true` if there is open input connection. bool get hasConnection => @@ -36,9 +37,10 @@ mixin RawEditorStateTextInputClientMixin on EditorState /// Opens or closes input connection based on the current state of /// [focusNode] and [value]. void openOrCloseConnection() { - if (widget.focusNode.hasFocus && widget.focusNode.consumeKeyboardToken()) { + if (widget.configurations.focusNode.hasFocus && + widget.configurations.focusNode.consumeKeyboardToken()) { openConnectionIfNeeded(); - } else if (!widget.focusNode.hasFocus) { + } else if (!widget.configurations.focusNode.hasFocus) { closeConnectionIfNeeded(); } } @@ -54,14 +56,16 @@ mixin RawEditorStateTextInputClientMixin on EditorState this, TextInputConfiguration( inputType: TextInputType.multiline, - readOnly: widget.readOnly, - inputAction: TextInputAction.newline, - enableSuggestions: !widget.readOnly, - keyboardAppearance: widget.keyboardAppearance, - textCapitalization: widget.textCapitalization, - allowedMimeTypes: widget.contentInsertionConfiguration == null - ? const [] - : widget.contentInsertionConfiguration!.allowedMimeTypes, + readOnly: widget.configurations.isReadOnly, + inputAction: widget.configurations.textInputAction, + enableSuggestions: !widget.configurations.isReadOnly, + keyboardAppearance: widget.configurations.keyboardAppearance, + textCapitalization: widget.configurations.textCapitalization, + allowedMimeTypes: + widget.configurations.contentInsertionConfiguration == null + ? const [] + : widget.configurations.contentInsertionConfiguration! + .allowedMimeTypes, ), ); @@ -187,9 +191,10 @@ mixin RawEditorStateTextInputClientMixin on EditorState final cursorPosition = value.selection.extentOffset; final diff = getDiff(oldText, text, cursorPosition); if (diff.deleted.isEmpty && diff.inserted.isEmpty) { - widget.controller.updateSelection(value.selection, ChangeSource.LOCAL); + widget.configurations.controller + .updateSelection(value.selection, ChangeSource.local); } else { - widget.controller.replaceText( + widget.configurations.controller.replaceText( diff.start, diff.deleted.length, diff.inserted, value.selection); } } diff --git a/lib/src/widgets/raw_editor/raw_editor_text_boundaries.dart b/lib/src/widgets/raw_editor/raw_editor_text_boundaries.dart new file mode 100644 index 00000000..d89d0665 --- /dev/null +++ b/lib/src/widgets/raw_editor/raw_editor_text_boundaries.dart @@ -0,0 +1,292 @@ +import 'dart:math' as math; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart' show TextLayoutMetrics; + +/// An interface for retrieving the logical text boundary +/// (left-closed-right-open) +/// at a given location in a document. +/// +/// Depending on the implementation of the [QuillEditorTextBoundary], the input +/// [TextPosition] can either point to a code unit, or a position between 2 code +/// units (which can be visually represented by the caret if the selection were +/// to collapse to that position). +/// +/// For example, [QuillEditorLineBreak] interprets the input [TextPosition] as a caret +/// location, since in Flutter the caret is generally painted between the +/// character the [TextPosition] points to and its previous character, and +/// [QuillEditorLineBreak] cares about the affinity of the input [TextPosition]. Most +/// other text boundaries however, interpret the input [TextPosition] as the +/// location of a code unit in the document, since it's easier to reason about +/// the text boundary given a code unit in the text. +/// +/// To convert a "code-unit-based" [QuillEditorTextBoundary] to "caret-location-based", +/// use the [QuillEditorCollapsedSelectionBoundary] combinator. +abstract class QuillEditorTextBoundary { + const QuillEditorTextBoundary(); + + TextEditingValue get textEditingValue; + + /// Returns the leading text boundary at the given location, inclusive. + TextPosition getLeadingTextBoundaryAt(TextPosition position); + + /// Returns the trailing text boundary at the given location, exclusive. + TextPosition getTrailingTextBoundaryAt(TextPosition position); + + TextRange getTextBoundaryAt(TextPosition position) { + return TextRange( + start: getLeadingTextBoundaryAt(position).offset, + end: getTrailingTextBoundaryAt(position).offset, + ); + } +} + +// ----------------------------- Text Boundaries ----------------------------- + +// The word modifier generally removes the word boundaries around white spaces +// (and newlines), IOW white spaces and some other punctuations are considered +// a part of the next word in the search direction. +class QuillEditorWhitespaceBoundary extends QuillEditorTextBoundary { + const QuillEditorWhitespaceBoundary(this.textEditingValue); + + @override + final TextEditingValue textEditingValue; + + @override + TextPosition getLeadingTextBoundaryAt(TextPosition position) { + for (var index = position.offset; index >= 0; index -= 1) { + if (!TextLayoutMetrics.isWhitespace( + textEditingValue.text.codeUnitAt(index))) { + return TextPosition(offset: index); + } + } + return const TextPosition(offset: 0); + } + + @override + TextPosition getTrailingTextBoundaryAt(TextPosition position) { + for (var index = position.offset; + index < textEditingValue.text.length; + index += 1) { + if (!TextLayoutMetrics.isWhitespace( + textEditingValue.text.codeUnitAt(index))) { + return TextPosition(offset: index + 1); + } + } + return TextPosition(offset: textEditingValue.text.length); + } +} + +// Most apps delete the entire grapheme when the backspace key is pressed. +// Also always put the new caret location to character boundaries to avoid +// sending malformed UTF-16 code units to the paragraph builder. +class QuillEditorCharacterBoundary extends QuillEditorTextBoundary { + const QuillEditorCharacterBoundary(this.textEditingValue); + + @override + final TextEditingValue textEditingValue; + + @override + TextPosition getLeadingTextBoundaryAt(TextPosition position) { + final int endOffset = + math.min(position.offset + 1, textEditingValue.text.length); + return TextPosition( + offset: + CharacterRange.at(textEditingValue.text, position.offset, endOffset) + .stringBeforeLength, + ); + } + + @override + TextPosition getTrailingTextBoundaryAt(TextPosition position) { + final int endOffset = + math.min(position.offset + 1, textEditingValue.text.length); + final range = + CharacterRange.at(textEditingValue.text, position.offset, endOffset); + return TextPosition( + offset: textEditingValue.text.length - range.stringAfterLength, + ); + } + + @override + TextRange getTextBoundaryAt(TextPosition position) { + final int endOffset = + math.min(position.offset + 1, textEditingValue.text.length); + final range = + CharacterRange.at(textEditingValue.text, position.offset, endOffset); + return TextRange( + start: range.stringBeforeLength, + end: textEditingValue.text.length - range.stringAfterLength, + ); + } +} + +// [UAX #29](https://unicode.org/reports/tr29/) defined word boundaries. +class QuillEditorWordBoundary extends QuillEditorTextBoundary { + const QuillEditorWordBoundary(this.textLayout, this.textEditingValue); + + final TextLayoutMetrics textLayout; + + @override + final TextEditingValue textEditingValue; + + @override + TextPosition getLeadingTextBoundaryAt(TextPosition position) { + return TextPosition( + offset: textLayout.getWordBoundary(position).start, + // Word boundary seems to always report downstream on many platforms. + affinity: + TextAffinity.downstream, // ignore: avoid_redundant_argument_values + ); + } + + @override + TextPosition getTrailingTextBoundaryAt(TextPosition position) { + return TextPosition( + offset: textLayout.getWordBoundary(position).end, + // Word boundary seems to always report downstream on many platforms. + affinity: + TextAffinity.downstream, // ignore: avoid_redundant_argument_values + ); + } +} + +// The linebreaks of the current text layout. The input [TextPosition]s are +// interpreted as caret locations because [TextPainter.getLineAtOffset] is +// text-affinity-aware. +class QuillEditorLineBreak extends QuillEditorTextBoundary { + const QuillEditorLineBreak(this.textLayout, this.textEditingValue); + + final TextLayoutMetrics textLayout; + + @override + final TextEditingValue textEditingValue; + + @override + TextPosition getLeadingTextBoundaryAt(TextPosition position) { + return TextPosition( + offset: textLayout.getLineAtOffset(position).start, + ); + } + + @override + TextPosition getTrailingTextBoundaryAt(TextPosition position) { + return TextPosition( + offset: textLayout.getLineAtOffset(position).end, + affinity: TextAffinity.upstream, + ); + } +} + +// The document boundary is unique and is a constant function of the input +// position. +class QuillEditorDocumentBoundary extends QuillEditorTextBoundary { + const QuillEditorDocumentBoundary(this.textEditingValue); + + @override + final TextEditingValue textEditingValue; + + @override + TextPosition getLeadingTextBoundaryAt(TextPosition position) => + const TextPosition(offset: 0); + + @override + TextPosition getTrailingTextBoundaryAt(TextPosition position) { + return TextPosition( + offset: textEditingValue.text.length, + affinity: TextAffinity.upstream, + ); + } +} + +// ------------------------ Text Boundary Combinators ------------------------ + +// Expands the innerTextBoundary with outerTextBoundary. +class QuillEditorExpandedTextBoundary extends QuillEditorTextBoundary { + QuillEditorExpandedTextBoundary( + this.innerTextBoundary, this.outerTextBoundary); + + final QuillEditorTextBoundary innerTextBoundary; + final QuillEditorTextBoundary outerTextBoundary; + + @override + TextEditingValue get textEditingValue { + assert(innerTextBoundary.textEditingValue == + outerTextBoundary.textEditingValue); + return innerTextBoundary.textEditingValue; + } + + @override + TextPosition getLeadingTextBoundaryAt(TextPosition position) { + return outerTextBoundary.getLeadingTextBoundaryAt( + innerTextBoundary.getLeadingTextBoundaryAt(position), + ); + } + + @override + TextPosition getTrailingTextBoundaryAt(TextPosition position) { + return outerTextBoundary.getTrailingTextBoundaryAt( + innerTextBoundary.getTrailingTextBoundaryAt(position), + ); + } +} + +// Force the innerTextBoundary to interpret the input [TextPosition]s as caret +// locations instead of code unit positions. +// +// The innerTextBoundary must be a [_TextBoundary] that interprets the input +// [TextPosition]s as code unit positions. +class QuillEditorCollapsedSelectionBoundary extends QuillEditorTextBoundary { + QuillEditorCollapsedSelectionBoundary(this.innerTextBoundary, this.isForward); + + final QuillEditorTextBoundary innerTextBoundary; + final bool isForward; + + @override + TextEditingValue get textEditingValue => innerTextBoundary.textEditingValue; + + @override + TextPosition getLeadingTextBoundaryAt(TextPosition position) { + return isForward + ? innerTextBoundary.getLeadingTextBoundaryAt(position) + : position.offset <= 0 + ? const TextPosition(offset: 0) + : innerTextBoundary.getLeadingTextBoundaryAt( + TextPosition(offset: position.offset - 1)); + } + + @override + TextPosition getTrailingTextBoundaryAt(TextPosition position) { + return isForward + ? innerTextBoundary.getTrailingTextBoundaryAt(position) + : position.offset <= 0 + ? const TextPosition(offset: 0) + : innerTextBoundary.getTrailingTextBoundaryAt( + TextPosition(offset: position.offset - 1)); + } +} + +// A _TextBoundary that creates a [TextRange] where its start is from the +// specified leading text boundary and its end is from the specified trailing +// text boundary. +class QuillEditorMixedBoundary extends QuillEditorTextBoundary { + QuillEditorMixedBoundary(this.leadingTextBoundary, this.trailingTextBoundary); + + final QuillEditorTextBoundary leadingTextBoundary; + final QuillEditorTextBoundary trailingTextBoundary; + + @override + TextEditingValue get textEditingValue { + assert(leadingTextBoundary.textEditingValue == + trailingTextBoundary.textEditingValue); + return leadingTextBoundary.textEditingValue; + } + + @override + TextPosition getLeadingTextBoundaryAt(TextPosition position) => + leadingTextBoundary.getLeadingTextBoundaryAt(position); + + @override + TextPosition getTrailingTextBoundaryAt(TextPosition position) => + trailingTextBoundary.getTrailingTextBoundaryAt(position); +} diff --git a/lib/src/widgets/style_widgets/bullet_point.dart b/lib/src/widgets/style_widgets/bullet_point.dart index 8b5fce70..1dd1b4cf 100644 --- a/lib/src/widgets/style_widgets/bullet_point.dart +++ b/lib/src/widgets/style_widgets/bullet_point.dart @@ -1,12 +1,12 @@ import 'package:flutter/material.dart'; -class QuillBulletPoint extends StatelessWidget { - const QuillBulletPoint({ +class QuillEditorBulletPoint extends StatelessWidget { + const QuillEditorBulletPoint({ required this.style, required this.width, this.padding = 0, - Key? key, - }) : super(key: key); + super.key, + }); final TextStyle style; final double width; diff --git a/lib/src/widgets/style_widgets/checkbox_point.dart b/lib/src/widgets/style_widgets/checkbox_point.dart index 1276a152..5eb82686 100644 --- a/lib/src/widgets/style_widgets/checkbox_point.dart +++ b/lib/src/widgets/style_widgets/checkbox_point.dart @@ -1,10 +1,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_animate/flutter_animate.dart'; -import '../../utils/extensions/build_context.dart'; +import '../../extensions/quill_provider.dart'; -class CheckboxPoint extends StatefulWidget { - const CheckboxPoint({ +class QuillEditorCheckboxPoint extends StatefulWidget { + const QuillEditorCheckboxPoint({ required this.size, required this.value, required this.enabled, @@ -20,10 +20,11 @@ class CheckboxPoint extends StatefulWidget { final QuillCheckboxBuilder? uiBuilder; @override - _CheckboxPointState createState() => _CheckboxPointState(); + QuillEditorCheckboxPointState createState() => + QuillEditorCheckboxPointState(); } -class _CheckboxPointState extends State { +class QuillEditorCheckboxPointState extends State { @override Widget build(BuildContext context) { final uiBuilder = widget.uiBuilder; @@ -78,11 +79,11 @@ class _CheckboxPointState extends State { if (context.requireQuillSharedConfigurations.animationConfigurations .checkBoxPointItem) { return Animate( - effects: [ - const SlideEffect( + effects: const [ + SlideEffect( duration: Duration(milliseconds: 70), ), - const ScaleEffect( + ScaleEffect( duration: Duration(milliseconds: 70), ) ], diff --git a/lib/src/widgets/style_widgets/number_point.dart b/lib/src/widgets/style_widgets/number_point.dart index 54d5ebc9..30b5590e 100644 --- a/lib/src/widgets/style_widgets/number_point.dart +++ b/lib/src/widgets/style_widgets/number_point.dart @@ -3,8 +3,8 @@ import 'package:flutter/material.dart'; import '../../models/documents/attribute.dart'; import '../text_block.dart'; -class QuillNumberPoint extends StatelessWidget { - const QuillNumberPoint({ +class QuillEditorNumberPoint extends StatelessWidget { + const QuillEditorNumberPoint({ required this.index, required this.indentLevelCounts, required this.count, @@ -13,8 +13,8 @@ class QuillNumberPoint extends StatelessWidget { required this.attrs, this.withDot = true, this.padding = 0.0, - Key? key, - }) : super(key: key); + super.key, + }); final int index; final Map indentLevelCounts; diff --git a/lib/src/widgets/text_block.dart b/lib/src/widgets/text_block.dart index d10596fe..1f70c9af 100644 --- a/lib/src/widgets/text_block.dart +++ b/lib/src/widgets/text_block.dart @@ -1,12 +1,12 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; +import '../extensions/quill_provider.dart'; import '../models/documents/attribute.dart'; import '../models/documents/nodes/block.dart'; import '../models/documents/nodes/line.dart'; import '../models/structs/vertical_spacing.dart'; import '../utils/delta.dart'; -import '../utils/extensions/build_context.dart'; import 'box.dart'; import 'controller.dart'; import 'cursor.dart'; @@ -146,7 +146,13 @@ class EditableTextBlock extends StatelessWidget { index++; final editableTextLine = EditableTextLine( line, - _buildLeading(context, line, index, indentLevelCounts, count), + _buildLeading( + context: context, + line: line, + index: index, + indentLevelCounts: indentLevelCounts, + count: count, + ), TextLine( line: line, textDirection: textDirection, @@ -194,14 +200,19 @@ class EditableTextBlock extends StatelessWidget { } } - Widget? _buildLeading(BuildContext context, Line line, int index, - Map indentLevelCounts, int count) { + Widget? _buildLeading({ + required BuildContext context, + required Line line, + required int index, + required Map indentLevelCounts, + required int count, + }) { final defaultStyles = QuillStyles.getStyles(context, false)!; final fontSize = defaultStyles.paragraph?.style.fontSize ?? 16; final attrs = line.style.attributes; if (attrs[Attribute.list.key] == Attribute.ol) { - return QuillNumberPoint( + return QuillEditorNumberPoint( index: index, indentLevelCounts: indentLevelCounts, count: count, @@ -213,7 +224,7 @@ class EditableTextBlock extends StatelessWidget { } if (attrs[Attribute.list.key] == Attribute.ul) { - return QuillBulletPoint( + return QuillEditorBulletPoint( style: defaultStyles.leading!.style.copyWith(fontWeight: FontWeight.bold), width: fontSize * 2, @@ -223,7 +234,7 @@ class EditableTextBlock extends StatelessWidget { if (attrs[Attribute.list.key] == Attribute.checked || attrs[Attribute.list.key] == Attribute.unchecked) { - return CheckboxPoint( + return QuillEditorCheckboxPoint( size: fontSize, value: attrs[Attribute.list.key] == Attribute.checked, enabled: !readOnly, @@ -233,7 +244,7 @@ class EditableTextBlock extends StatelessWidget { } if (attrs.containsKey(Attribute.codeBlock.key) && context.requireQuillEditorElementOptions.codeBlock.enableLineNumbers) { - return QuillNumberPoint( + return QuillEditorNumberPoint( index: index, indentLevelCounts: indentLevelCounts, count: count, @@ -278,7 +289,11 @@ class EditableTextBlock extends StatelessWidget { } VerticalSpacing _getSpacingForLine( - Line node, int index, int count, DefaultStyles? defaultStyles) { + Line node, + int index, + int count, + DefaultStyles? defaultStyles, + ) { var top = 0.0, bottom = 0.0; final attrs = block.style.attributes; @@ -298,10 +313,10 @@ class EditableTextBlock extends StatelessWidget { bottom = defaultStyles.h3!.verticalSpacing.bottom; break; default: - throw 'Invalid level $level'; + throw ArgumentError('Invalid level $level'); } } else { - late VerticalSpacing lineSpacing; + final VerticalSpacing lineSpacing; if (attrs.containsKey(Attribute.blockQuote.key)) { lineSpacing = defaultStyles!.quote!.lineSpacing; } else if (attrs.containsKey(Attribute.indent.key)) { @@ -336,21 +351,18 @@ class RenderEditableTextBlock extends RenderEditableContainerBox implements RenderEditableBox { RenderEditableTextBlock({ required Block block, - required TextDirection textDirection, + required super.textDirection, required EdgeInsetsGeometry padding, - required double scrollBottomInset, + required super.scrollBottomInset, required Decoration decoration, - List? children, + super.children, EdgeInsets contentPadding = EdgeInsets.zero, }) : _decoration = decoration, _configuration = ImageConfiguration(textDirection: textDirection), _savedPadding = padding, _contentPadding = contentPadding, super( - children: children, container: block, - textDirection: textDirection, - scrollBottomInset: scrollBottomInset, padding: padding.add(contentPadding), ); @@ -500,12 +512,18 @@ class RenderEditableTextBlock extends RenderEditableContainerBox TextSelectionPoint getBaseEndpointForSelection(TextSelection selection) { if (selection.isCollapsed) { return TextSelectionPoint( - Offset(0, preferredLineHeight(selection.extent)) + - getOffsetForCaret(selection.extent), - null); + Offset(0, preferredLineHeight(selection.extent)) + + getOffsetForCaret(selection.extent), + null, + ); } - final baseNode = container.queryChild(selection.start, false).node; + final baseNode = container + .queryChild( + selection.start, + false, + ) + .node; var baseChild = firstChild; while (baseChild != null) { if (baseChild.container == baseNode) { @@ -516,19 +534,26 @@ class RenderEditableTextBlock extends RenderEditableContainerBox assert(baseChild != null); final basePoint = baseChild!.getBaseEndpointForSelection( - localSelection(baseChild.container, selection, true)); + localSelection( + baseChild.container, + selection, + true, + ), + ); return TextSelectionPoint( - basePoint.point + (baseChild.parentData as BoxParentData).offset, - basePoint.direction); + basePoint.point + (baseChild.parentData as BoxParentData).offset, + basePoint.direction, + ); } @override TextSelectionPoint getExtentEndpointForSelection(TextSelection selection) { if (selection.isCollapsed) { return TextSelectionPoint( - Offset(0, preferredLineHeight(selection.extent)) + - getOffsetForCaret(selection.extent), - null); + Offset(0, preferredLineHeight(selection.extent)) + + getOffsetForCaret(selection.extent), + null, + ); } final extentNode = container.queryChild(selection.end, false).node; @@ -543,10 +568,16 @@ class RenderEditableTextBlock extends RenderEditableContainerBox assert(extentChild != null); final extentPoint = extentChild!.getExtentEndpointForSelection( - localSelection(extentChild.container, selection, true)); + localSelection( + extentChild.container, + selection, + true, + ), + ); return TextSelectionPoint( - extentPoint.point + (extentChild.parentData as BoxParentData).offset, - extentPoint.direction); + extentPoint.point + (extentChild.parentData as BoxParentData).offset, + extentPoint.direction, + ); } @override @@ -576,8 +607,10 @@ class RenderEditableTextBlock extends RenderEditableContainerBox offset.translate(decorationPadding.left, decorationPadding.top); _painter!.paint(context.canvas, decorationOffset, filledConfiguration); if (debugSaveCount != context.canvas.getSaveCount()) { - throw '${_decoration.runtimeType} painter had mismatching save and ' - 'restore calls.'; + throw StateError( + '${_decoration.runtimeType} painter had mismatching save and ' + 'restore calls.', + ); } if (decoration.isComplex) { context.setIsComplexHint(); @@ -629,9 +662,7 @@ class _EditableBlock extends MultiChildRenderObjectWidget { required this.scrollBottomInset, required this.decoration, required this.contentPadding, - required List children, - Key? key}) - : super(key: key, children: children); + required super.children}); final Block block; final TextDirection textDirection; diff --git a/lib/src/widgets/text_line.dart b/lib/src/widgets/text_line.dart index 6dd4f793..5f095dc7 100644 --- a/lib/src/widgets/text_line.dart +++ b/lib/src/widgets/text_line.dart @@ -43,8 +43,8 @@ class TextLine extends StatefulWidget { this.customStyleBuilder, this.customRecognizerBuilder, this.customLinkPrefixes = const [], - Key? key, - }) : super(key: key); + super.key, + }); final Line line; final TextDirection? textDirection; @@ -88,9 +88,9 @@ class _TextLineState extends State { // In editing mode it depends on the platform: - // Desktop platforms (macos, linux, windows): - // only allow Meta(Control)+Click combinations - if (isDesktop()) { + // Desktop platforms (macOS, Linux, Windows): + // only allow Meta (Control) + Click combinations + if (isDesktop(supportWeb: false)) { return _metaOrControlPressed; } // Mobile platforms (ios, android): always allow but we install a @@ -143,15 +143,23 @@ class _TextLineState extends State { var embed = widget.line.children.single as Embed; // Creates correct node for custom embed if (embed.value.type == BlockEmbed.customType) { - embed = Embed(CustomBlockEmbed.fromJsonString(embed.value.data)); + embed = Embed( + CustomBlockEmbed.fromJsonString(embed.value.data), + ); } final embedBuilder = widget.embedBuilder(embed); if (embedBuilder.expanded) { // Creates correct node for custom embed final lineStyle = _getLineStyle(widget.styles); return EmbedProxy( - embedBuilder.build(context, widget.controller, embed, widget.readOnly, - false, lineStyle), + embedBuilder.build( + context, + widget.controller, + embed, + widget.readOnly, + false, + lineStyle, + ), ); } } @@ -167,12 +175,13 @@ class _TextLineState extends State { textScaleFactor: MediaQuery.textScaleFactorOf(context), ); return RichTextProxy( - textStyle: textSpan.style!, - textAlign: textAlign, - textDirection: widget.textDirection!, - strutStyle: strutStyle, - locale: Localizations.localeOf(context), - child: child); + textStyle: textSpan.style!, + textAlign: textAlign, + textDirection: widget.textDirection!, + strutStyle: strutStyle, + locale: Localizations.localeOf(context), + child: child, + ); } InlineSpan _getTextSpanForWholeLine(BuildContext context) { @@ -294,14 +303,14 @@ class _TextLineState extends State { if (widget.customStyleBuilder == null) { return textStyle; } - attributes.keys.forEach((key) { + for (final key in attributes.keys) { final attr = attributes[key]; if (attr != null) { /// Custom Attribute final customAttr = widget.customStyleBuilder!.call(attr); textStyle = textStyle.merge(customAttr); } - }); + } return textStyle; } @@ -434,7 +443,7 @@ class _TextLineState extends State { } if (isLink && canLaunchLinks) { - if (isDesktop() || widget.readOnly) { + if (isDesktop(supportWeb: true) || widget.readOnly) { _linkRecognizers[segment] = TapGestureRecognizer() ..onTap = () => _tapNodeLink(segment); } else { @@ -508,19 +517,19 @@ class _TextLineState extends State { class EditableTextLine extends RenderObjectWidget { const EditableTextLine( - this.line, - this.leading, - this.body, - this.indentWidth, - this.verticalSpacing, - this.textDirection, - this.textSelection, - this.color, - this.enableInteractiveSelection, - this.hasFocus, - this.devicePixelRatio, - this.cursorCont, - ); + this.line, + this.leading, + this.body, + this.indentWidth, + this.verticalSpacing, + this.textDirection, + this.textSelection, + this.color, + this.enableInteractiveSelection, + this.hasFocus, + this.devicePixelRatio, + this.cursorCont, + {super.key}); final Line line; final Widget? leading; @@ -581,21 +590,22 @@ class EditableTextLine extends RenderObjectWidget { } } -enum TextLineSlot { LEADING, BODY } +enum TextLineSlot { leading, body } class RenderEditableTextLine extends RenderEditableBox { /// Creates new editable paragraph render box. RenderEditableTextLine( - this.line, - this.textDirection, - this.textSelection, - this.enableInteractiveSelection, - this.hasFocus, - this.devicePixelRatio, - this.padding, - this.color, - this.cursorCont, - this.inlineCodeStyle); + this.line, + this.textDirection, + this.textSelection, + this.enableInteractiveSelection, + this.hasFocus, + this.devicePixelRatio, + this.padding, + this.color, + this.cursorCont, + this.inlineCodeStyle, + ); RenderBox? _leading; RenderContentProxyBox? _body; @@ -715,11 +725,11 @@ class RenderEditableTextLine extends RenderEditableBox { } void setLeading(RenderBox? l) { - _leading = _updateChild(_leading, l, TextLineSlot.LEADING); + _leading = _updateChild(_leading, l, TextLineSlot.leading); } void setBody(RenderContentProxyBox? b) { - _body = _updateChild(_body, b, TextLineSlot.BODY) as RenderContentProxyBox?; + _body = _updateChild(_body, b, TextLineSlot.body) as RenderContentProxyBox?; } void setInlineCodeStyle(InlineCodeStyle newStyle) { @@ -744,7 +754,10 @@ class RenderEditableTextLine extends RenderEditableBox { } RenderBox? _updateChild( - RenderBox? old, RenderBox? newChild, TextLineSlot slot) { + RenderBox? old, + RenderBox? newChild, + TextLineSlot slot, + ) { if (old != null) { dropChild(old); children.remove(slot); @@ -800,8 +813,9 @@ class RenderEditableTextLine extends RenderEditableBox { assert(boxes.isNotEmpty); final targetBox = first ? boxes.first : boxes.last; return TextSelectionPoint( - Offset(first ? targetBox.start : targetBox.end, targetBox.bottom), - targetBox.direction); + Offset(first ? targetBox.start : targetBox.end, targetBox.bottom), + targetBox.direction, + ); } @override @@ -814,9 +828,12 @@ class RenderEditableTextLine extends RenderEditableBox { .where((element) => element.top < lineDy && element.bottom > lineDy) .toList(growable: false); return TextRange( - start: - getPositionForOffset(Offset(lineBoxes.first.left, lineDy)).offset, - end: getPositionForOffset(Offset(lineBoxes.last.right, lineDy)).offset); + start: getPositionForOffset( + Offset(lineBoxes.first.left, lineDy), + ).offset, + end: getPositionForOffset( + Offset(lineBoxes.last.right, lineDy), + ).offset); } @override @@ -886,7 +903,9 @@ class RenderEditableTextLine extends RenderEditableBox { /// of the cursor for iOS is approximate and obtained through an eyeball /// comparison. void _computeCaretPrototype() { - if (isAppleOS()) { + // If the cursor is taller only on iOS and not AppleOS then we should check + // only for iOS instead of AppleOS (macOS for example) + if (isIOS(supportWeb: true)) { _caretPrototype = Rect.fromLTWH(0, 0, cursorWidth, cursorHeight + 2); } else { _caretPrototype = Rect.fromLTWH(0, 2, cursorWidth, cursorHeight - 4.0); @@ -1090,8 +1109,13 @@ class RenderEditableTextLine extends RenderEditableBox { } else { final parentData = _leading!.parentData as BoxParentData; final effectiveOffset = offset + parentData.offset; - context.paintChild(_leading!, - Offset(size.width - _leading!.size.width, effectiveOffset.dy)); + context.paintChild( + _leading!, + Offset( + size.width - _leading!.size.width, + effectiveOffset.dy, + ), + ); } } @@ -1106,18 +1130,29 @@ class RenderEditableTextLine extends RenderEditableBox { continue; } final textRange = TextSelection( - baseOffset: item.offset, extentOffset: item.offset + item.length); + baseOffset: item.offset, + extentOffset: item.offset + item.length, + ); final rects = _body!.getBoxesForSelection(textRange); final paint = Paint()..color = inlineCodeStyle.backgroundColor!; for (final box in rects) { final rect = box.toRect().translate(0, 1).shift(effectiveOffset); if (inlineCodeStyle.radius == null) { final paintRect = Rect.fromLTRB( - rect.left - 2, rect.top, rect.right + 2, rect.bottom); + rect.left - 2, + rect.top, + rect.right + 2, + rect.bottom, + ); context.canvas.drawRect(paintRect, paint); } else { - final paintRect = RRect.fromLTRBR(rect.left - 2, rect.top, - rect.right + 2, rect.bottom, inlineCodeStyle.radius!); + final paintRect = RRect.fromLTRBR( + rect.left - 2, + rect.top, + rect.right + 2, + rect.bottom, + inlineCodeStyle.radius!, + ); context.canvas.drawRRect(paintRect, paint); } } @@ -1154,10 +1189,20 @@ class RenderEditableTextLine extends RenderEditableBox { if (line.isEmpty && textSelection.baseOffset <= line.offset && textSelection.extentOffset > line.offset) { - final lineHeight = - preferredLineHeight(TextPosition(offset: line.offset)); - _selectedRects - ?.add(TextBox.fromLTRBD(0, 0, 3, lineHeight, textDirection)); + final lineHeight = preferredLineHeight( + TextPosition( + offset: line.offset, + ), + ); + _selectedRects?.add( + TextBox.fromLTRBD( + 0, + 0, + 3, + lineHeight, + textDirection, + ), + ); } _paintSelection(context, effectiveOffset); @@ -1179,12 +1224,18 @@ class RenderEditableTextLine extends RenderEditableBox { ? TextPosition( offset: cursorCont.floatingCursorTextPosition.value!.offset - line.documentOffset, - affinity: cursorCont.floatingCursorTextPosition.value!.affinity) + affinity: cursorCont.floatingCursorTextPosition.value!.affinity, + ) : TextPosition( offset: textSelection.extentOffset - line.documentOffset, - affinity: textSelection.base.affinity); + affinity: textSelection.base.affinity, + ); _cursorPainter.paint( - context.canvas, effectiveOffset, position, lineHasEmbed); + context.canvas, + effectiveOffset, + position, + lineHasEmbed, + ); } @override @@ -1192,29 +1243,35 @@ class RenderEditableTextLine extends RenderEditableBox { if (_leading != null) { final childParentData = _leading!.parentData as BoxParentData; final isHit = result.addWithPaintOffset( - offset: childParentData.offset, - position: position, - hitTest: (result, transformed) { - assert(transformed == position - childParentData.offset); - return _leading!.hitTest(result, position: transformed); - }); + offset: childParentData.offset, + position: position, + hitTest: (result, transformed) { + assert(transformed == position - childParentData.offset); + return _leading!.hitTest(result, position: transformed); + }, + ); if (isHit) return true; } if (_body == null) return false; final parentData = _body!.parentData as BoxParentData; return result.addWithPaintOffset( - offset: parentData.offset, - position: position, - hitTest: (result, position) { - return _body!.hitTest(result, position: position); - }); + offset: parentData.offset, + position: position, + hitTest: (result, position) { + return _body!.hitTest(result, position: position); + }, + ); } @override Rect getLocalRectForCaret(TextPosition position) { final caretOffset = getOffsetForCaret(position); - var rect = - Rect.fromLTWH(0, 0, cursorWidth, cursorHeight).shift(caretOffset); + var rect = Rect.fromLTWH( + 0, + 0, + cursorWidth, + cursorHeight, + ).shift(caretOffset); final cursorOffset = cursorCont.style.offset; // Add additional cursor offset (generally only if on iOS). if (cursorOffset != null) rect = rect.shift(cursorOffset); @@ -1244,7 +1301,7 @@ class RenderEditableTextLine extends RenderEditableBox { } class _TextLineElement extends RenderObjectElement { - _TextLineElement(EditableTextLine line) : super(line); + _TextLineElement(EditableTextLine super.line); final Map _slotToChildren = {}; @@ -1272,16 +1329,16 @@ class _TextLineElement extends RenderObjectElement { @override void mount(Element? parent, dynamic newSlot) { super.mount(parent, newSlot); - _mountChild(widget.leading, TextLineSlot.LEADING); - _mountChild(widget.body, TextLineSlot.BODY); + _mountChild(widget.leading, TextLineSlot.leading); + _mountChild(widget.body, TextLineSlot.body); } @override void update(EditableTextLine newWidget) { super.update(newWidget); assert(widget == newWidget); - _updateChild(widget.leading, TextLineSlot.LEADING); - _updateChild(widget.body, TextLineSlot.BODY); + _updateChild(widget.leading, TextLineSlot.leading); + _updateChild(widget.body, TextLineSlot.body); } @override @@ -1318,10 +1375,10 @@ class _TextLineElement extends RenderObjectElement { void _updateRenderObject(RenderBox? child, TextLineSlot? slot) { switch (slot) { - case TextLineSlot.LEADING: + case TextLineSlot.leading: renderObject.setLeading(child); break; - case TextLineSlot.BODY: + case TextLineSlot.body: renderObject.setBody(child as RenderContentProxyBox?); break; default: diff --git a/lib/src/widgets/text_selection.dart b/lib/src/widgets/text_selection.dart index c13f9952..c51d414d 100644 --- a/lib/src/widgets/text_selection.dart +++ b/lib/src/widgets/text_selection.dart @@ -21,22 +21,17 @@ TextSelection localSelection(Node node, TextSelection selection, fromParent) { /// The text position that a give selection handle manipulates. Dragging the /// [start] handle always moves the [start]/[baseOffset] of the selection. -enum _TextSelectionHandlePosition { START, END } +enum _TextSelectionHandlePosition { start, end } /// internal use, used to get drag direction information class DragTextSelection extends TextSelection { const DragTextSelection({ - TextAffinity affinity = TextAffinity.downstream, - int baseOffset = 0, - int extentOffset = 0, - bool isDirectional = false, + super.affinity, + super.baseOffset = 0, + super.extentOffset = 0, + super.isDirectional, this.first = true, - }) : super( - baseOffset: baseOffset, - extentOffset: extentOffset, - affinity: affinity, - isDirectional: isDirectional, - ); + }); final bool first; @@ -236,7 +231,7 @@ class EditorTextSelectionOverlay { Widget _buildHandle( BuildContext context, _TextSelectionHandlePosition position) { if (_selection.isCollapsed && - position == _TextSelectionHandlePosition.END) { + position == _TextSelectionHandlePosition.end) { return Container(); } return Visibility( @@ -279,21 +274,23 @@ class EditorTextSelectionOverlay { } void _handleSelectionHandleChanged( - TextSelection? newSelection, _TextSelectionHandlePosition position) { + TextSelection? newSelection, + _TextSelectionHandlePosition position, + ) { TextPosition textPosition; switch (position) { - case _TextSelectionHandlePosition.START: + case _TextSelectionHandlePosition.start: textPosition = newSelection != null ? newSelection.base : const TextPosition(offset: 0); break; - case _TextSelectionHandlePosition.END: + case _TextSelectionHandlePosition.end: textPosition = newSelection != null ? newSelection.extent : const TextPosition(offset: 0); break; default: - throw 'Invalid position'; + throw ArgumentError('Invalid position'); } final currSelection = newSelection != null @@ -302,7 +299,7 @@ class EditorTextSelectionOverlay { extentOffset: newSelection.extentOffset, affinity: newSelection.affinity, isDirectional: newSelection.isDirectional, - first: position == _TextSelectionHandlePosition.START, + first: position == _TextSelectionHandlePosition.start, ) : null; @@ -344,10 +341,10 @@ class EditorTextSelectionOverlay { _handles = [ OverlayEntry( builder: (context) => - _buildHandle(context, _TextSelectionHandlePosition.START)), + _buildHandle(context, _TextSelectionHandlePosition.start)), OverlayEntry( builder: (context) => - _buildHandle(context, _TextSelectionHandlePosition.END)), + _buildHandle(context, _TextSelectionHandlePosition.end)), ]; Overlay.of(context, rootOverlay: true, debugRequiredFor: debugRequiredFor) @@ -375,8 +372,7 @@ class _TextSelectionHandleOverlay extends StatefulWidget { required this.onSelectionHandleTapped, required this.selectionControls, this.dragStartBehavior = DragStartBehavior.start, - Key? key, - }) : super(key: key); + }); final TextSelection selection; final _TextSelectionHandlePosition position; @@ -394,12 +390,12 @@ class _TextSelectionHandleOverlay extends StatefulWidget { ValueListenable get _visibility { switch (position) { - case _TextSelectionHandlePosition.START: + case _TextSelectionHandlePosition.start: return renderObject.selectionStartInViewport; - case _TextSelectionHandlePosition.END: + case _TextSelectionHandlePosition.end: return renderObject.selectionEndInViewport; default: - throw 'Invalid position'; + throw ArgumentError('Invalid position'); } } } @@ -449,7 +445,7 @@ class _TextSelectionHandleOverlayState } void _handleDragStart(DragStartDetails details) { - final textPosition = widget.position == _TextSelectionHandlePosition.START + final textPosition = widget.position == _TextSelectionHandlePosition.start ? widget.selection.base : widget.selection.extent; final lineHeight = widget.renderObject.preferredLineHeight(textPosition); @@ -470,7 +466,7 @@ class _TextSelectionHandleOverlayState widget.selection.extentOffset >= widget.selection.baseOffset; TextSelection newSelection; switch (widget.position) { - case _TextSelectionHandlePosition.START: + case _TextSelectionHandlePosition.start: newSelection = TextSelection( baseOffset: isNormalized ? position.offset : widget.selection.baseOffset, @@ -478,7 +474,7 @@ class _TextSelectionHandleOverlayState isNormalized ? widget.selection.extentOffset : position.offset, ); break; - case _TextSelectionHandlePosition.END: + case _TextSelectionHandlePosition.end: newSelection = TextSelection( baseOffset: isNormalized ? widget.selection.baseOffset : position.offset, @@ -487,7 +483,7 @@ class _TextSelectionHandleOverlayState ); break; default: - throw 'Invalid widget.position'; + throw ArgumentError('Invalid widget.position'); } if (newSelection.baseOffset >= newSelection.extentOffset) { @@ -498,9 +494,7 @@ class _TextSelectionHandleOverlayState } void _handleTap() { - if (widget.onSelectionHandleTapped != null) { - widget.onSelectionHandleTapped!(); - } + widget.onSelectionHandleTapped?.call(); } @override @@ -509,7 +503,7 @@ class _TextSelectionHandleOverlayState TextSelectionHandleType? type; switch (widget.position) { - case _TextSelectionHandlePosition.START: + case _TextSelectionHandlePosition.start: layerLink = widget.startHandleLayerLink; type = _chooseType( widget.renderObject.textDirection, @@ -517,7 +511,7 @@ class _TextSelectionHandleOverlayState TextSelectionHandleType.right, ); break; - case _TextSelectionHandlePosition.END: + case _TextSelectionHandlePosition.end: // For collapsed selections, we shouldn't be building the [end] handle. assert(!widget.selection.isCollapsed); layerLink = widget.endHandleLayerLink; @@ -535,7 +529,7 @@ class _TextSelectionHandleOverlayState // May have to use getSelectionBoxes instead of preferredLineHeight. // or expose TextStyle on the render object and calculate // preferredLineHeight / style.height - final textPosition = widget.position == _TextSelectionHandlePosition.START + final textPosition = widget.position == _TextSelectionHandlePosition.start ? widget.selection.base : widget.selection.extent; final lineHeight = widget.renderObject.preferredLineHeight(textPosition); @@ -651,8 +645,8 @@ class EditorTextSelectionGestureDetector extends StatefulWidget { this.onDragSelectionEnd, this.behavior, this.detectWordBoundary = true, - Key? key, - }) : super(key: key); + super.key, + }); /// Called for every tap down including every tap down that's part of a /// double click or a long press, except touches that include enough movement @@ -756,9 +750,8 @@ class _EditorTextSelectionGestureDetectorState // The down handler is force-run on success of a single tap and optimistically // run before a long press success. void _handleTapDown(TapDownDetails details) { - if (widget.onTapDown != null) { - widget.onTapDown!(details); - } + widget.onTapDown?.call(details); + // This isn't detected as a double tap gesture in the gesture recognizer // because it's 2 single taps, each of which may do different things // depending on whether it's a single tap, the first tap of a double tap, @@ -767,9 +760,8 @@ class _EditorTextSelectionGestureDetectorState _isWithinDoubleTapTolerance(details.globalPosition)) { // If there was already a previous tap, the second down hold/tap is a // double tap down. - if (widget.onDoubleTapDown != null) { - widget.onDoubleTapDown!(details); - } + + widget.onDoubleTapDown?.call(details); _doubleTapTimer!.cancel(); _doubleTapTimeout(); @@ -779,9 +771,7 @@ class _EditorTextSelectionGestureDetectorState void _handleTapUp(TapUpDetails details) { if (!_isDoubleTap) { - if (widget.onSingleTapUp != null) { - widget.onSingleTapUp!(details); - } + widget.onSingleTapUp?.call(details); _lastTapOffset = details.globalPosition; _doubleTapTimer = Timer(kDoubleTapTimeout, _doubleTapTimeout); } @@ -789,21 +779,17 @@ class _EditorTextSelectionGestureDetectorState } void _handleTapCancel() { - if (widget.onSingleTapCancel != null) { - widget.onSingleTapCancel!(); - } + widget.onSingleTapCancel?.call(); } // added secondary tap function for mouse right click to show toolbar void _handleSecondaryTapDown(TapDownDetails details) { if (widget.onSecondaryTapDown != null) { - widget.onSecondaryTapDown!(details); + widget.onSecondaryTapDown?.call(details); } if (_doubleTapTimer != null && _isWithinDoubleTapTolerance(details.globalPosition)) { - if (widget.onSecondaryDoubleTapDown != null) { - widget.onSecondaryDoubleTapDown!(details); - } + widget.onSecondaryDoubleTapDown?.call(details); _doubleTapTimer!.cancel(); _doubleTapTimeout(); @@ -813,9 +799,7 @@ class _EditorTextSelectionGestureDetectorState void _handleSecondaryTapUp(TapUpDetails details) { if (!_isSecondaryDoubleTap) { - if (widget.onSecondarySingleTapUp != null) { - widget.onSecondarySingleTapUp!(details); - } + widget.onSecondarySingleTapUp?.call(details); _lastTapOffset = details.globalPosition; _doubleTapTimer = Timer(kDoubleTapTimeout, _doubleTapTimeout); } @@ -823,9 +807,7 @@ class _EditorTextSelectionGestureDetectorState } void _handleSecondaryTapCancel() { - if (widget.onSecondarySingleTapCancel != null) { - widget.onSecondarySingleTapCancel!(); - } + widget.onSecondarySingleTapCancel?.call(); } DragStartDetails? _lastDragStartDetails; @@ -835,15 +817,15 @@ class _EditorTextSelectionGestureDetectorState void _handleDragStart(DragStartDetails details) { assert(_lastDragStartDetails == null); _lastDragStartDetails = details; - if (widget.onDragSelectionStart != null) { - widget.onDragSelectionStart!(details); - } + widget.onDragSelectionStart?.call(details); } void _handleDragUpdate(DragUpdateDetails details) { _lastDragUpdateDetails = details; - _dragUpdateThrottleTimer ??= - Timer(const Duration(milliseconds: 50), _handleDragUpdateThrottled); + _dragUpdateThrottleTimer ??= Timer( + const Duration(milliseconds: 50), + _handleDragUpdateThrottled, + ); } /// Drag updates are being throttled to avoid excessive text layouts in text @@ -872,9 +854,9 @@ class _EditorTextSelectionGestureDetectorState _dragUpdateThrottleTimer!.cancel(); _handleDragUpdateThrottled(); } - if (widget.onDragSelectionEnd != null) { - widget.onDragSelectionEnd!(details); - } + + widget.onDragSelectionEnd?.call(details); + _dragUpdateThrottleTimer = null; _lastDragStartDetails = null; _lastDragUpdateDetails = null; @@ -883,32 +865,30 @@ class _EditorTextSelectionGestureDetectorState void _forcePressStarted(ForcePressDetails details) { _doubleTapTimer?.cancel(); _doubleTapTimer = null; - if (widget.onForcePressStart != null) { - widget.onForcePressStart!(details); - } + widget.onForcePressStart?.call(details); } void _forcePressEnded(ForcePressDetails details) { if (widget.onForcePressEnd != null) { - widget.onForcePressEnd!(details); + widget.onForcePressEnd?.call(details); } } void _handleLongPressStart(LongPressStartDetails details) { - if (!_isDoubleTap && widget.onSingleLongTapStart != null) { - widget.onSingleLongTapStart!(details); + if (!_isDoubleTap) { + widget.onSingleLongTapStart?.call(details); } } void _handleLongPressMoveUpdate(LongPressMoveUpdateDetails details) { - if (!_isDoubleTap && widget.onSingleLongTapMoveUpdate != null) { - widget.onSingleLongTapMoveUpdate!(details); + if (!_isDoubleTap) { + widget.onSingleLongTapMoveUpdate?.call(details); } } void _handleLongPressEnd(LongPressEndDetails details) { - if (!_isDoubleTap && widget.onSingleLongTapEnd != null) { - widget.onSingleLongTapEnd!(details); + if (!_isDoubleTap) { + widget.onSingleLongTapEnd?.call(details); } _isDoubleTap = false; } @@ -1017,8 +997,8 @@ class _EditorTextSelectionGestureDetectorState // underlying input. class _TransparentTapGestureRecognizer extends TapGestureRecognizer { _TransparentTapGestureRecognizer({ - Object? debugOwner, - }) : super(debugOwner: debugOwner); + super.debugOwner, + }); @override void rejectGesture(int pointer) { diff --git a/lib/src/widgets/toolbar/base_toolbar.dart b/lib/src/widgets/toolbar/base_toolbar.dart index b422c713..a40a0a2b 100644 --- a/lib/src/widgets/toolbar/base_toolbar.dart +++ b/lib/src/widgets/toolbar/base_toolbar.dart @@ -1,16 +1,15 @@ import 'package:flutter/material.dart'; -import 'package:i18n_extension/i18n_widget.dart'; import '../../../flutter_quill.dart' show QuillBaseToolbarProvider, defaultToolbarSize; +import '../../l10n/widgets/localizations.dart'; import '../../models/config/toolbar/base_configurations.dart'; -import '../../utils/extensions/build_context.dart'; import 'buttons/arrow_indicated_list.dart'; export '../../models/config/toolbar/buttons/base.dart'; export '../../models/config/toolbar/configurations.dart'; export 'buttons/clear_format.dart'; -export 'buttons/color.dart'; +export 'buttons/color/color.dart'; export 'buttons/custom_button.dart'; export 'buttons/font_family.dart'; export 'buttons/font_size.dart'; @@ -49,8 +48,7 @@ class QuillBaseToolbar extends StatelessWidget implements PreferredSizeWidget { @override Widget build(BuildContext context) { final toolbarSize = configurations.toolbarSize; - return I18n( - initialLocale: context.quillSharedConfigurations?.locale, + return FlutterQuillLocalizationsWidget( child: QuillBaseToolbarProvider( toolbarConfigurations: configurations, child: Builder( @@ -102,12 +100,12 @@ class QuillToolbarDivider extends StatelessWidget { }); /// Provides a horizontal divider for vertical toolbar. - const QuillToolbarDivider.horizontal({Color? color, double? space}) - : this(Axis.horizontal, color: color, space: space); + const QuillToolbarDivider.horizontal({Key? key, Color? color, double? space}) + : this(Axis.horizontal, color: color, space: space, key: key); /// Provides a horizontal divider for horizontal toolbar. - const QuillToolbarDivider.vertical({Color? color, double? space}) - : this(Axis.vertical, color: color, space: space); + const QuillToolbarDivider.vertical({Key? key, Color? color, double? space}) + : this(Axis.vertical, color: color, space: space, key: key); /// The axis along which the toolbar is. final Axis axis; diff --git a/lib/src/widgets/toolbar/buttons/arrow_indicated_list.dart b/lib/src/widgets/toolbar/buttons/arrow_indicated_list.dart index 146fcc95..c8ccd1bc 100644 --- a/lib/src/widgets/toolbar/buttons/arrow_indicated_list.dart +++ b/lib/src/widgets/toolbar/buttons/arrow_indicated_list.dart @@ -10,18 +10,18 @@ class QuillToolbarArrowIndicatedButtonList extends StatefulWidget { const QuillToolbarArrowIndicatedButtonList({ required this.axis, required this.buttons, - Key? key, - }) : super(key: key); + super.key, + }); final Axis axis; final List buttons; @override - _QuillToolbarArrowIndicatedButtonListState createState() => - _QuillToolbarArrowIndicatedButtonListState(); + QuillToolbarArrowIndicatedButtonListState createState() => + QuillToolbarArrowIndicatedButtonListState(); } -class _QuillToolbarArrowIndicatedButtonListState +class QuillToolbarArrowIndicatedButtonListState extends State with WidgetsBindingObserver { final ScrollController _controller = ScrollController(); diff --git a/lib/src/widgets/toolbar/buttons/clear_format.dart b/lib/src/widgets/toolbar/buttons/clear_format.dart index 2e2c4e9d..e5f4afc7 100644 --- a/lib/src/widgets/toolbar/buttons/clear_format.dart +++ b/lib/src/widgets/toolbar/buttons/clear_format.dart @@ -1,9 +1,9 @@ import 'package:flutter/material.dart'; -import '../../../../translations.dart'; +import '../../../extensions/quill_provider.dart'; +import '../../../l10n/extensions/localizations.dart'; import '../../../models/documents/attribute.dart'; import '../../../models/themes/quill_icon_theme.dart'; -import '../../../utils/extensions/build_context.dart'; import '../../controller.dart'; import '../base_toolbar.dart'; @@ -27,6 +27,13 @@ class QuillToolbarClearFormatButton extends StatelessWidget { return iconSize ?? baseFontSize; } + double _iconButtonFactor(BuildContext context) { + final baseIconFactor = + baseButtonExtraOptions(context).globalIconButtonFactor; + final iconButtonFactor = options.iconButtonFactor; + return iconButtonFactor ?? baseIconFactor; + } + VoidCallback? _afterButtonPressed(BuildContext context) { return options.afterButtonPressed ?? baseButtonExtraOptions(context).afterButtonPressed; @@ -49,7 +56,7 @@ class QuillToolbarClearFormatButton extends StatelessWidget { String _tooltip(BuildContext context) { return options.tooltip ?? baseButtonExtraOptions(context).tooltip ?? - ('Clear format'.i18n); + (context.loc.clearFormat); } void _sharedOnPressed() { @@ -69,6 +76,7 @@ class QuillToolbarClearFormatButton extends StatelessWidget { final iconTheme = _iconTheme(context); final tooltip = _tooltip(context); final iconSize = _iconSize(context); + final iconButtonFactor = _iconButtonFactor(context); final iconData = _iconData(context); final childBuilder = @@ -82,6 +90,7 @@ class QuillToolbarClearFormatButton extends StatelessWidget { controller: controller, iconData: iconData, iconSize: iconSize, + iconButtonFactor: iconButtonFactor, iconTheme: iconTheme, tooltip: tooltip, ), @@ -105,7 +114,7 @@ class QuillToolbarClearFormatButton extends StatelessWidget { tooltip: tooltip, highlightElevation: 0, hoverElevation: 0, - size: iconSize * kIconButtonFactor, + size: iconSize * iconButtonFactor, icon: Icon(iconData, size: iconSize, color: iconColor), fillColor: fillColor, borderRadius: iconTheme?.borderRadius ?? 2, diff --git a/lib/src/widgets/toolbar/buttons/color.dart b/lib/src/widgets/toolbar/buttons/color.dart deleted file mode 100644 index ed431fea..00000000 --- a/lib/src/widgets/toolbar/buttons/color.dart +++ /dev/null @@ -1,347 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_colorpicker/flutter_colorpicker.dart'; - -import '../../../models/documents/attribute.dart'; -import '../../../models/documents/style.dart'; -import '../../../models/themes/quill_icon_theme.dart'; -import '../../../translations/toolbar.i18n.dart'; -import '../../../utils/color.dart'; -import '../../../utils/extensions/build_context.dart'; -import '../../controller.dart'; -import '../base_toolbar.dart'; - -/// Controls color styles. -/// -/// When pressed, this button displays overlay toolbar with -/// buttons for each color. -class QuillToolbarColorButton extends StatefulWidget { - const QuillToolbarColorButton({ - required this.controller, - required this.isBackground, - this.options = const QuillToolbarColorButtonOptions(), - super.key, - }); - - /// Is this background color button or font color - final bool isBackground; - final QuillController controller; - final QuillToolbarColorButtonOptions options; - - @override - _QuillToolbarColorButtonState createState() => - _QuillToolbarColorButtonState(); -} - -class _QuillToolbarColorButtonState extends State { - late bool _isToggledColor; - late bool _isToggledBackground; - late bool _isWhite; - late bool _isWhiteBackground; - - Style get _selectionStyle => widget.controller.getSelectionStyle(); - - void _didChangeEditingValue() { - setState(() { - _isToggledColor = - _getIsToggledColor(widget.controller.getSelectionStyle().attributes); - _isToggledBackground = _getIsToggledBackground( - widget.controller.getSelectionStyle().attributes); - _isWhite = _isToggledColor && - _selectionStyle.attributes['color']!.value == '#ffffff'; - _isWhiteBackground = _isToggledBackground && - _selectionStyle.attributes['background']!.value == '#ffffff'; - }); - } - - @override - void initState() { - super.initState(); - _isToggledColor = _getIsToggledColor(_selectionStyle.attributes); - _isToggledBackground = _getIsToggledBackground(_selectionStyle.attributes); - _isWhite = _isToggledColor && - _selectionStyle.attributes['color']!.value == '#ffffff'; - _isWhiteBackground = _isToggledBackground && - _selectionStyle.attributes['background']!.value == '#ffffff'; - widget.controller.addListener(_didChangeEditingValue); - } - - bool _getIsToggledColor(Map attrs) { - return attrs.containsKey(Attribute.color.key); - } - - bool _getIsToggledBackground(Map attrs) { - return attrs.containsKey(Attribute.background.key); - } - - @override - void didUpdateWidget(covariant QuillToolbarColorButton oldWidget) { - super.didUpdateWidget(oldWidget); - if (oldWidget.controller != widget.controller) { - oldWidget.controller.removeListener(_didChangeEditingValue); - widget.controller.addListener(_didChangeEditingValue); - _isToggledColor = _getIsToggledColor(_selectionStyle.attributes); - _isToggledBackground = - _getIsToggledBackground(_selectionStyle.attributes); - _isWhite = _isToggledColor && - _selectionStyle.attributes['color']!.value == '#ffffff'; - _isWhiteBackground = _isToggledBackground && - _selectionStyle.attributes['background']!.value == '#ffffff'; - } - } - - @override - void dispose() { - widget.controller.removeListener(_didChangeEditingValue); - super.dispose(); - } - - QuillToolbarColorButtonOptions get options { - return widget.options; - } - - QuillController get controller { - return widget.controller; - } - - double get iconSize { - final baseFontSize = baseButtonExtraOptions.globalIconSize; - final iconSize = options.iconSize; - return iconSize ?? baseFontSize; - } - - VoidCallback? get afterButtonPressed { - return options.afterButtonPressed ?? - baseButtonExtraOptions.afterButtonPressed; - } - - QuillIconTheme? get iconTheme { - return options.iconTheme ?? baseButtonExtraOptions.iconTheme; - } - - QuillToolbarBaseButtonOptions get baseButtonExtraOptions { - return context.requireQuillToolbarBaseButtonOptions; - } - - IconData get iconData { - return options.iconData ?? - baseButtonExtraOptions.iconData ?? - (widget.isBackground ? Icons.color_lens : Icons.format_color_fill); - } - - String get tooltip { - return options.tooltip ?? - baseButtonExtraOptions.tooltip ?? - (widget.isBackground ? 'Font color'.i18n : 'Background color'.i18n); - } - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - final iconColor = _isToggledColor && !widget.isBackground && !_isWhite - ? stringToColor(_selectionStyle.attributes['color']!.value) - : (iconTheme?.iconUnselectedColor ?? theme.iconTheme.color); - - final iconColorBackground = - _isToggledBackground && widget.isBackground && !_isWhiteBackground - ? stringToColor(_selectionStyle.attributes['background']!.value) - : (iconTheme?.iconUnselectedColor ?? theme.iconTheme.color); - - final fillColor = _isToggledColor && !widget.isBackground && _isWhite - ? stringToColor('#ffffff') - : (iconTheme?.iconUnselectedFillColor ?? theme.canvasColor); - final fillColorBackground = - _isToggledBackground && widget.isBackground && _isWhiteBackground - ? stringToColor('#ffffff') - : (iconTheme?.iconUnselectedFillColor ?? theme.canvasColor); - - final childBuilder = - options.childBuilder ?? baseButtonExtraOptions.childBuilder; - if (childBuilder != null) { - // if the caller using Cupertino app he might need to wrap the builder - // with Material() widget - return childBuilder( - options, - QuillToolbarColorButtonExtraOptions( - controller: controller, - context: context, - onPressed: () { - _showColorPicker(); - afterButtonPressed?.call(); - }, - iconColor: null, - iconColorBackground: iconColorBackground, - fillColor: fillColor, - fillColorBackground: fillColorBackground, - ), - ); - } - - return QuillToolbarIconButton( - tooltip: tooltip, - highlightElevation: 0, - hoverElevation: 0, - size: iconSize * kIconButtonFactor, - icon: Icon(iconData, - size: iconSize, - color: widget.isBackground ? iconColorBackground : iconColor), - fillColor: widget.isBackground ? fillColorBackground : fillColor, - borderRadius: iconTheme?.borderRadius ?? 2, - onPressed: _showColorPicker, - afterPressed: afterButtonPressed, - ); - } - - void _changeColor(BuildContext context, Color color) { - var hex = colorToHex(color); - hex = '#$hex'; - widget.controller.formatSelection( - widget.isBackground ? BackgroundAttribute(hex) : ColorAttribute(hex)); - } - - void _showColorPicker() { - var pickerType = 'material'; - - var selectedColor = Colors.black; - - if (_isToggledColor) { - selectedColor = widget.isBackground - ? hexToColor(_selectionStyle.attributes['background']?.value) - : hexToColor(_selectionStyle.attributes['color']?.value); - } - - final hexController = - TextEditingController(text: colorToHex(selectedColor)); - late void Function(void Function()) colorBoxSetState; - - // TODO: Please make this dialog only responsible for picking the color - // so in the future we will add an option for showing custom method - // defined by the developer for picking dialog, and it will return - // color hex - - // I won't for now to save some time for the refactoring - showDialog( - context: context, - barrierColor: options.dialogBarrierColor ?? - context.requireQuillSharedConfigurations.dialogBarrierColor, - builder: (context) => StatefulBuilder(builder: (context, dlgSetState) { - return AlertDialog( - title: Text('Select Color'.i18n), - actions: [ - TextButton( - onPressed: () { - Navigator.of(context).pop(); - }, - child: Text('OK'.i18n)), - ], - backgroundColor: Theme.of(context).canvasColor, - content: SingleChildScrollView( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Row( - children: [ - TextButton( - onPressed: () { - dlgSetState(() { - pickerType = 'material'; - }); - }, - child: Text('Material'.i18n)), - TextButton( - onPressed: () { - dlgSetState(() { - pickerType = 'color'; - }); - }, - child: Text('Color'.i18n)), - ], - ), - Column(children: [ - if (pickerType == 'material') - MaterialPicker( - pickerColor: selectedColor, - onColorChanged: (color) { - _changeColor(context, color); - Navigator.of(context).pop(); - }, - ), - if (pickerType == 'color') - ColorPicker( - pickerColor: selectedColor, - onColorChanged: (color) { - _changeColor(context, color); - hexController.text = colorToHex(color); - selectedColor = color; - colorBoxSetState(() {}); - }, - ), - const SizedBox( - height: 10, - ), - Row( - children: [ - SizedBox( - width: 100, - height: 60, - child: TextFormField( - controller: hexController, - onChanged: (value) { - selectedColor = hexToColor(value); - _changeColor(context, selectedColor); - - colorBoxSetState(() {}); - }, - decoration: InputDecoration( - labelText: 'Hex'.i18n, - border: const OutlineInputBorder(), - ), - ), - ), - const SizedBox( - width: 10, - ), - StatefulBuilder(builder: (context, mcolorBoxSetState) { - colorBoxSetState = mcolorBoxSetState; - return Container( - width: 25, - height: 25, - decoration: BoxDecoration( - border: Border.all( - color: Colors.black45, - ), - color: selectedColor, - borderRadius: BorderRadius.circular(5), - ), - ); - }), - ], - ), - ]) - ], - ), - )); - }), - ); - } - - Color hexToColor(String? hexString) { - if (hexString == null) { - return Colors.black; - } - final hexRegex = RegExp(r'([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6})$'); - - hexString = hexString.replaceAll('#', ''); - if (!hexRegex.hasMatch(hexString)) { - return Colors.black; - } - - final buffer = StringBuffer(); - if (hexString.length == 6 || hexString.length == 7) buffer.write('ff'); - buffer.write(hexString); - return Color(int.tryParse(buffer.toString(), radix: 16) ?? 0xFF000000); - } - - String colorToHex(Color color) { - return color.value.toRadixString(16).padLeft(8, '0').toUpperCase(); - } -} diff --git a/lib/src/widgets/toolbar/buttons/color/color.dart b/lib/src/widgets/toolbar/buttons/color/color.dart new file mode 100644 index 00000000..cd45880d --- /dev/null +++ b/lib/src/widgets/toolbar/buttons/color/color.dart @@ -0,0 +1,264 @@ +import 'package:flutter/material.dart'; + +import '../../../../extensions/quill_provider.dart'; +import '../../../../l10n/extensions/localizations.dart'; +import '../../../../l10n/widgets/localizations.dart'; +import '../../../../models/documents/attribute.dart'; +import '../../../../models/documents/style.dart'; +import '../../../../models/themes/quill_icon_theme.dart'; +import '../../../../utils/color.dart'; +import '../../../controller.dart'; +import '../../../utils/provider.dart'; +import '../../base_toolbar.dart'; +import 'dialog.dart'; + +/// Controls color styles. +/// +/// When pressed, this button displays overlay toolbar with +/// buttons for each color. +class QuillToolbarColorButton extends StatefulWidget { + const QuillToolbarColorButton({ + required this.controller, + required this.isBackground, + this.options = const QuillToolbarColorButtonOptions(), + super.key, + }); + + /// Is this background color button or font color + final bool isBackground; + final QuillController controller; + final QuillToolbarColorButtonOptions options; + + @override + QuillToolbarColorButtonState createState() => QuillToolbarColorButtonState(); +} + +class QuillToolbarColorButtonState extends State { + late bool _isToggledColor; + late bool _isToggledBackground; + late bool _isWhite; + late bool _isWhiteBackground; + + Style get _selectionStyle => widget.controller.getSelectionStyle(); + + void _didChangeEditingValue() { + setState(() { + _isToggledColor = + _getIsToggledColor(widget.controller.getSelectionStyle().attributes); + _isToggledBackground = _getIsToggledBackground( + widget.controller.getSelectionStyle().attributes); + _isWhite = _isToggledColor && + _selectionStyle.attributes['color']!.value == '#ffffff'; + _isWhiteBackground = _isToggledBackground && + _selectionStyle.attributes['background']!.value == '#ffffff'; + }); + } + + @override + void initState() { + super.initState(); + _isToggledColor = _getIsToggledColor(_selectionStyle.attributes); + _isToggledBackground = _getIsToggledBackground(_selectionStyle.attributes); + _isWhite = _isToggledColor && + _selectionStyle.attributes['color']!.value == '#ffffff'; + _isWhiteBackground = _isToggledBackground && + _selectionStyle.attributes['background']!.value == '#ffffff'; + widget.controller.addListener(_didChangeEditingValue); + } + + bool _getIsToggledColor(Map attrs) { + return attrs.containsKey(Attribute.color.key); + } + + bool _getIsToggledBackground(Map attrs) { + return attrs.containsKey(Attribute.background.key); + } + + @override + void didUpdateWidget(covariant QuillToolbarColorButton oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.controller != widget.controller) { + oldWidget.controller.removeListener(_didChangeEditingValue); + widget.controller.addListener(_didChangeEditingValue); + _isToggledColor = _getIsToggledColor(_selectionStyle.attributes); + _isToggledBackground = + _getIsToggledBackground(_selectionStyle.attributes); + _isWhite = _isToggledColor && + _selectionStyle.attributes['color']!.value == '#ffffff'; + _isWhiteBackground = _isToggledBackground && + _selectionStyle.attributes['background']!.value == '#ffffff'; + } + } + + @override + void dispose() { + widget.controller.removeListener(_didChangeEditingValue); + super.dispose(); + } + + QuillToolbarColorButtonOptions get options { + return widget.options; + } + + QuillController get controller { + return widget.controller; + } + + double get iconSize { + final baseFontSize = baseButtonExtraOptions.globalIconSize; + final iconSize = options.iconSize; + return iconSize ?? baseFontSize; + } + + double get iconButtonFactor { + final baseIconFactor = baseButtonExtraOptions.globalIconButtonFactor; + final iconButtonFactor = options.iconButtonFactor; + return iconButtonFactor ?? baseIconFactor; + } + + VoidCallback? get afterButtonPressed { + return options.afterButtonPressed ?? + baseButtonExtraOptions.afterButtonPressed; + } + + QuillIconTheme? get iconTheme { + return options.iconTheme ?? baseButtonExtraOptions.iconTheme; + } + + QuillToolbarBaseButtonOptions get baseButtonExtraOptions { + return context.requireQuillToolbarBaseButtonOptions; + } + + IconData get iconData { + return options.iconData ?? + baseButtonExtraOptions.iconData ?? + (widget.isBackground ? Icons.format_color_fill : Icons.color_lens); + } + + String get tooltip { + return options.tooltip ?? + baseButtonExtraOptions.tooltip ?? + (widget.isBackground + ? context.loc.backgroundColor + : context.loc.fontColor); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final iconColor = _isToggledColor && !widget.isBackground && !_isWhite + ? stringToColor(_selectionStyle.attributes['color']!.value) + : (iconTheme?.iconUnselectedColor ?? theme.iconTheme.color); + + final iconColorBackground = + _isToggledBackground && widget.isBackground && !_isWhiteBackground + ? stringToColor(_selectionStyle.attributes['background']!.value) + : (iconTheme?.iconUnselectedColor ?? theme.iconTheme.color); + + final fillColor = _isToggledColor && !widget.isBackground && _isWhite + ? stringToColor('#ffffff') + : (iconTheme?.iconUnselectedFillColor ?? theme.canvasColor); + final fillColorBackground = + _isToggledBackground && widget.isBackground && _isWhiteBackground + ? stringToColor('#ffffff') + : (iconTheme?.iconUnselectedFillColor ?? theme.canvasColor); + + final childBuilder = + options.childBuilder ?? baseButtonExtraOptions.childBuilder; + if (childBuilder != null) { + // if the caller using Cupertino app he might need to wrap the builder + // with Material() widget + return childBuilder( + QuillToolbarColorButtonOptions( + afterButtonPressed: afterButtonPressed, + dialogBarrierColor: options.dialogBarrierColor, + tooltip: tooltip, + iconTheme: iconTheme, + iconSize: iconSize, + iconData: iconData, + iconButtonFactor: iconButtonFactor, + customOnPressedCallback: options.customOnPressedCallback, + ), + QuillToolbarColorButtonExtraOptions( + controller: controller, + context: context, + onPressed: () { + _showColorPicker(); + afterButtonPressed?.call(); + }, + iconColor: null, + iconColorBackground: iconColorBackground, + fillColor: fillColor, + fillColorBackground: fillColorBackground, + ), + ); + } + + return QuillToolbarIconButton( + tooltip: tooltip, + highlightElevation: 0, + hoverElevation: 0, + size: iconSize * iconButtonFactor, + icon: Icon(iconData, + size: iconSize, + color: widget.isBackground ? iconColorBackground : iconColor), + fillColor: widget.isBackground ? fillColorBackground : fillColor, + borderRadius: iconTheme?.borderRadius ?? 2, + onPressed: _showColorPicker, + afterPressed: afterButtonPressed, + ); + } + + void _changeColor(BuildContext context, Color color) { + var hex = colorToHex(color); + hex = '#$hex'; + widget.controller.formatSelection( + widget.isBackground ? BackgroundAttribute(hex) : ColorAttribute(hex), + ); + } + + Future _showColorPicker() async { + final customCallback = options.customOnPressedCallback; + if (customCallback != null) { + await customCallback(controller, widget.isBackground); + return; + } + showDialog( + context: context, + barrierColor: options.dialogBarrierColor ?? + context.requireQuillSharedConfigurations.dialogBarrierColor, + builder: (_) => QuillProvider.value( + value: context.requireQuillProvider, + child: FlutterQuillLocalizationsWidget( + child: ColorPickerDialog( + isBackground: widget.isBackground, + onRequestChangeColor: _changeColor, + isToggledColor: _isToggledColor, + selectionStyle: _selectionStyle, + ), + ), + ), + ); + } +} + +Color hexToColor(String? hexString) { + if (hexString == null) { + return Colors.black; + } + final hexRegex = RegExp(r'([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6})$'); + + hexString = hexString.replaceAll('#', ''); + if (!hexRegex.hasMatch(hexString)) { + return Colors.black; + } + + final buffer = StringBuffer(); + if (hexString.length == 6 || hexString.length == 7) buffer.write('ff'); + buffer.write(hexString); + return Color(int.tryParse(buffer.toString(), radix: 16) ?? 0xFF000000); +} + +String colorToHex(Color color) { + return color.value.toRadixString(16).padLeft(8, '0').toUpperCase(); +} diff --git a/lib/src/widgets/toolbar/buttons/color/dialog.dart b/lib/src/widgets/toolbar/buttons/color/dialog.dart new file mode 100644 index 00000000..a9806f51 --- /dev/null +++ b/lib/src/widgets/toolbar/buttons/color/dialog.dart @@ -0,0 +1,149 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_colorpicker/flutter_colorpicker.dart' + show ColorPicker, MaterialPicker, colorToHex; + +import '../../../../../translations.dart'; +import '../../../../models/documents/style.dart'; +import 'color.dart' show hexToColor; + +class ColorPickerDialog extends StatefulWidget { + const ColorPickerDialog({ + required this.isBackground, + required this.onRequestChangeColor, + required this.isToggledColor, + required this.selectionStyle, + super.key, + }); + final bool isBackground; + + final bool isToggledColor; + final Function(BuildContext context, Color color) onRequestChangeColor; + final Style selectionStyle; + + @override + State createState() => ColorPickerDialogState(); +} + +class ColorPickerDialogState extends State { + var pickerType = 'material'; + var selectedColor = Colors.black; + + late final TextEditingController hexController; + late void Function(void Function()) colorBoxSetState; + + @override + void initState() { + super.initState(); + hexController = TextEditingController(text: colorToHex(selectedColor)); + if (widget.isToggledColor) { + selectedColor = widget.isBackground + ? hexToColor(widget.selectionStyle.attributes['background']?.value) + : hexToColor(widget.selectionStyle.attributes['color']?.value); + } + } + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: Text(context.loc.selectColor), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: Text(context.loc.ok)), + ], + backgroundColor: Theme.of(context).canvasColor, + content: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + children: [ + TextButton( + onPressed: () { + setState(() { + pickerType = 'material'; + }); + }, + child: Text(context.loc.material)), + TextButton( + onPressed: () { + setState(() { + pickerType = 'color'; + }); + }, + child: Text(context.loc.color)), + ], + ), + Column( + children: [ + if (pickerType == 'material') + MaterialPicker( + pickerColor: selectedColor, + onColorChanged: (color) { + widget.onRequestChangeColor(context, color); + Navigator.of(context).pop(); + }, + ), + if (pickerType == 'color') + ColorPicker( + pickerColor: selectedColor, + onColorChanged: (color) { + widget.onRequestChangeColor(context, color); + hexController.text = colorToHex(color); + selectedColor = color; + colorBoxSetState(() {}); + }, + ), + const SizedBox( + height: 10, + ), + Row( + children: [ + SizedBox( + width: 100, + height: 60, + child: TextFormField( + controller: hexController, + onChanged: (value) { + selectedColor = hexToColor(value); + widget.onRequestChangeColor(context, selectedColor); + + colorBoxSetState(() {}); + }, + decoration: InputDecoration( + labelText: context.loc.hex, + border: const OutlineInputBorder(), + ), + ), + ), + const SizedBox( + width: 10, + ), + StatefulBuilder( + builder: (context, mcolorBoxSetState) { + colorBoxSetState = mcolorBoxSetState; + return Container( + width: 25, + height: 25, + decoration: BoxDecoration( + border: Border.all( + color: Colors.black45, + ), + color: selectedColor, + borderRadius: BorderRadius.circular(5), + ), + ); + }, + ), + ], + ), + ], + ) + ], + ), + ), + ); + } +} diff --git a/lib/src/widgets/toolbar/buttons/custom_button.dart b/lib/src/widgets/toolbar/buttons/custom_button.dart index 50745e74..f306a440 100644 --- a/lib/src/widgets/toolbar/buttons/custom_button.dart +++ b/lib/src/widgets/toolbar/buttons/custom_button.dart @@ -1,41 +1,92 @@ import 'package:flutter/material.dart'; +import '../../../extensions/quill_provider.dart'; import '../../../models/themes/quill_icon_theme.dart'; +import '../../controller.dart'; import '../base_toolbar.dart'; -class CustomButton extends StatelessWidget { - const CustomButton({ - required this.onPressed, - required this.icon, - this.iconColor, - this.iconSize = kDefaultIconSize, - this.iconTheme, - this.afterButtonPressed, - this.tooltip, +class QuillToolbarCustomButton extends StatelessWidget { + const QuillToolbarCustomButton({ + required this.options, + required this.controller, super.key, }); - final VoidCallback? onPressed; - final IconData? icon; - final Color? iconColor; - final double iconSize; - final QuillIconTheme? iconTheme; - final VoidCallback? afterButtonPressed; - final String? tooltip; + final QuillController controller; + final QuillToolbarCustomButtonOptions options; + + double _iconSize(BuildContext context) { + final baseFontSize = baseButtonExtraOptions(context).globalIconSize; + final iconSize = options.iconSize; + return iconSize ?? baseFontSize; + } + + double _iconButtonFactor(BuildContext context) { + final baseIconFactor = + baseButtonExtraOptions(context).globalIconButtonFactor; + final iconButtonFactor = options.iconButtonFactor; + return iconButtonFactor ?? baseIconFactor; + } + + VoidCallback? _afterButtonPressed(BuildContext context) { + return options.afterButtonPressed ?? + baseButtonExtraOptions(context).afterButtonPressed; + } + + QuillIconTheme? _iconTheme(BuildContext context) { + return options.iconTheme ?? baseButtonExtraOptions(context).iconTheme; + } + + QuillToolbarBaseButtonOptions baseButtonExtraOptions(BuildContext context) { + return context.requireQuillToolbarBaseButtonOptions; + } + + String? _tooltip(BuildContext context) { + return options.tooltip ?? baseButtonExtraOptions(context).tooltip; + } + + void _onPressed(BuildContext context) { + options.onPressed?.call(); + _afterButtonPressed(context)?.call(); + } @override Widget build(BuildContext context) { - final theme = Theme.of(context); + final iconTheme = _iconTheme(context); + final tooltip = _tooltip(context); + final iconSize = _iconSize(context); + final iconButtonFactor = _iconButtonFactor(context); + + final childBuilder = + options.childBuilder ?? baseButtonExtraOptions(context).childBuilder; + final afterButtonPressed = _afterButtonPressed(context); - final iconColor = iconTheme?.iconUnselectedColor ?? theme.iconTheme.color; + if (childBuilder != null) { + return childBuilder( + QuillToolbarCustomButtonOptions( + iconButtonFactor: iconButtonFactor, + iconSize: iconSize, + afterButtonPressed: afterButtonPressed, + controller: controller, + iconTheme: iconTheme, + tooltip: tooltip, + icon: options.icon, + ), + QuillToolbarCustomButtonExtraOptions( + context: context, + controller: controller, + onPressed: () => _onPressed(context), + ), + ); + } + + final theme = Theme.of(context); return QuillToolbarIconButton( - highlightElevation: 0, - hoverElevation: 0, - size: iconSize * kIconButtonFactor, - icon: Icon(icon, size: iconSize, color: iconColor), + size: iconSize * iconButtonFactor, + icon: options.icon, tooltip: tooltip, borderRadius: iconTheme?.borderRadius ?? 2, - onPressed: onPressed, + onPressed: () => _onPressed(context), afterPressed: afterButtonPressed, fillColor: iconTheme?.iconUnselectedFillColor ?? theme.canvasColor, ); diff --git a/lib/src/widgets/toolbar/buttons/font_family.dart b/lib/src/widgets/toolbar/buttons/font_family.dart index a82424a1..b90a1d0b 100644 --- a/lib/src/widgets/toolbar/buttons/font_family.dart +++ b/lib/src/widgets/toolbar/buttons/font_family.dart @@ -1,18 +1,19 @@ import 'package:flutter/material.dart'; import '../../../../extensions.dart'; +import '../../../extensions/quill_provider.dart'; +import '../../../l10n/extensions/localizations.dart'; import '../../../models/config/toolbar/buttons/font_family.dart'; import '../../../models/documents/attribute.dart'; import '../../../models/documents/style.dart'; import '../../../models/themes/quill_icon_theme.dart'; -import '../../../translations/toolbar.i18n.dart'; -import '../../../utils/extensions/build_context.dart'; import '../../controller.dart'; class QuillToolbarFontFamilyButton extends StatefulWidget { QuillToolbarFontFamilyButton({ required this.options, required this.controller, + required this.defaultDispalyText, super.key, }) : assert(options.rawItemsMap?.isNotEmpty ?? (true)), assert( @@ -21,16 +22,18 @@ class QuillToolbarFontFamilyButton extends StatefulWidget { final QuillToolbarFontFamilyButtonOptions options; + final String defaultDispalyText; + /// Since we can't get the state from the instace of the widget for comparing /// in [didUpdateWidget] then we will have to store reference here final QuillController controller; @override - _QuillToolbarFontFamilyButtonState createState() => - _QuillToolbarFontFamilyButtonState(); + QuillToolbarFontFamilyButtonState createState() => + QuillToolbarFontFamilyButtonState(); } -class _QuillToolbarFontFamilyButtonState +class QuillToolbarFontFamilyButtonState extends State { var _currentValue = ''; @@ -58,7 +61,7 @@ class _QuillToolbarFontFamilyButtonState } String get _defaultDisplayText { - return options.initialValue ?? 'Font'.i18n; + return options.initialValue ?? widget.defaultDispalyText; } @override @@ -94,7 +97,7 @@ class _QuillToolbarFontFamilyButtonState 'Nunito': 'nunito', 'Pacifico': 'pacifico', 'Roboto Mono': 'roboto-mono', - 'Clear'.i18n: 'Clear' + context.loc.clear: 'Clear' }; return rawItemsMap; } @@ -132,7 +135,7 @@ class _QuillToolbarFontFamilyButtonState String get tooltip { return options.tooltip ?? context.requireQuillToolbarBaseButtonOptions.tooltip ?? - 'Font family'.i18n; + context.loc.fontFamily; } void _onPressed() { @@ -176,7 +179,7 @@ class _QuillToolbarFontFamilyButtonState if (options.overrideTooltipByFontFamily) { effectiveTooltip = effectiveTooltip.isNotEmpty ? '$effectiveTooltip: $_currentValue' - : '${'Font'.i18n}: $_currentValue'; + : '${context.loc.font}: $_currentValue'; } return Tooltip(message: effectiveTooltip, child: child); }, diff --git a/lib/src/widgets/toolbar/buttons/font_size.dart b/lib/src/widgets/toolbar/buttons/font_size.dart index 419ad46a..621fc5c7 100644 --- a/lib/src/widgets/toolbar/buttons/font_size.dart +++ b/lib/src/widgets/toolbar/buttons/font_size.dart @@ -1,14 +1,20 @@ import 'package:flutter/material.dart'; import '../../../../extensions.dart'; -import '../../../../flutter_quill.dart'; -import '../../../translations/toolbar.i18n.dart'; +import '../../../extensions/quill_provider.dart'; +import '../../../l10n/extensions/localizations.dart'; +import '../../../models/config/quill_configurations.dart'; +import '../../../models/documents/attribute.dart'; +import '../../../models/documents/style.dart'; +import '../../../models/themes/quill_icon_theme.dart'; import '../../../utils/font.dart'; +import '../../controller.dart'; class QuillToolbarFontSizeButton extends StatefulWidget { QuillToolbarFontSizeButton({ required this.options, required this.controller, + required this.defaultDisplayText, super.key, }) : assert(options.rawItemsMap?.isNotEmpty ?? true), assert(options.initialValue == null || @@ -16,16 +22,18 @@ class QuillToolbarFontSizeButton extends StatefulWidget { final QuillToolbarFontSizeButtonOptions options; + final String defaultDisplayText; + /// Since we can't get the state from the instace of the widget for comparing /// in [didUpdateWidget] then we will have to store reference here final QuillController controller; @override - _QuillToolbarFontSizeButtonState createState() => - _QuillToolbarFontSizeButtonState(); + QuillToolbarFontSizeButtonState createState() => + QuillToolbarFontSizeButtonState(); } -class _QuillToolbarFontSizeButtonState +class QuillToolbarFontSizeButtonState extends State { String _currentValue = ''; @@ -37,16 +45,16 @@ class _QuillToolbarFontSizeButtonState final fontSizes = options.rawItemsMap ?? context.requireQuillToolbarConfigurations.fontSizesValues ?? { - 'Small'.i18n: 'small', - 'Large'.i18n: 'large', - 'Huge'.i18n: 'huge', - 'Clear'.i18n: '0' + context.loc.small: 'small', + context.loc.large: 'large', + context.loc.huge: 'huge', + context.loc.clear: '0' }; return fontSizes; } String get _defaultDisplayText { - return options.initialValue ?? 'Size'.i18n; + return options.initialValue ?? widget.defaultDisplayText; } Style get _selectionStyle => controller.getSelectionStyle(); @@ -110,6 +118,13 @@ class _QuillToolbarFontSizeButtonState return iconSize ?? baseFontSize; } + double get iconButtonFactor { + final baseIconFactor = + context.requireQuillToolbarBaseButtonOptions.globalIconButtonFactor; + final iconButtonFactor = options.iconButtonFactor; + return iconButtonFactor ?? baseIconFactor; + } + VoidCallback? get afterButtonPressed { return options.afterButtonPressed ?? context.requireQuillToolbarBaseButtonOptions.afterButtonPressed; @@ -123,7 +138,7 @@ class _QuillToolbarFontSizeButtonState String get tooltip { return options.tooltip ?? context.requireQuillToolbarBaseButtonOptions.tooltip ?? - 'Font size'.i18n; + context.loc.fontSize; } void _onPressed() { @@ -142,6 +157,7 @@ class _QuillToolbarFontSizeButtonState options.copyWith( tooltip: tooltip, iconSize: iconSize, + iconButtonFactor: iconButtonFactor, iconTheme: iconTheme, afterButtonPressed: afterButtonPressed, controller: controller, diff --git a/lib/src/widgets/toolbar/buttons/history.dart b/lib/src/widgets/toolbar/buttons/history.dart index 297b4b9f..bbf7c54f 100644 --- a/lib/src/widgets/toolbar/buttons/history.dart +++ b/lib/src/widgets/toolbar/buttons/history.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; -import '../../../../translations.dart'; -import '../../../utils/extensions/build_context.dart'; +import '../../../extensions/quill_provider.dart'; +import '../../../l10n/extensions/localizations.dart'; import '../../controller.dart'; import '../base_toolbar.dart'; @@ -16,11 +16,11 @@ class QuillToolbarHistoryButton extends StatefulWidget { final QuillController controller; @override - _QuillToolbarHistoryButtonState createState() => - _QuillToolbarHistoryButtonState(); + QuillToolbarHistoryButtonState createState() => + QuillToolbarHistoryButtonState(); } -class _QuillToolbarHistoryButtonState extends State { +class QuillToolbarHistoryButtonState extends State { late ThemeData theme; var _canPressed = false; @@ -53,14 +53,16 @@ class _QuillToolbarHistoryButtonState extends State { context.requireQuillToolbarBaseButtonOptions; final tooltip = options.tooltip ?? baseButtonConfigurations.tooltip ?? - (options.isUndo ? 'Undo'.i18n : 'Redo'.i18n); + (options.isUndo ? context.loc.undo : context.loc.redo); final iconData = options.iconData ?? baseButtonConfigurations.iconData ?? (options.isUndo ? Icons.undo_outlined : Icons.redo_outlined); final childBuilder = options.childBuilder ?? baseButtonConfigurations.childBuilder; - final iconSize = options.iconSize ?? - context.requireQuillToolbarBaseButtonOptions.globalIconSize; + final iconSize = + options.iconSize ?? baseButtonConfigurations.globalIconSize; + final iconButtonFactor = options.iconButtonFactor ?? + baseButtonConfigurations.globalIconButtonFactor; final iconTheme = options.iconTheme ?? baseButtonConfigurations.iconTheme; final afterButtonPressed = options.afterButtonPressed ?? @@ -74,6 +76,7 @@ class _QuillToolbarHistoryButtonState extends State { controller: controller, iconData: iconData, iconSize: iconSize, + iconButtonFactor: iconButtonFactor, iconTheme: iconTheme, tooltip: tooltip, ), @@ -96,7 +99,7 @@ class _QuillToolbarHistoryButtonState extends State { tooltip: tooltip, highlightElevation: 0, hoverElevation: 0, - size: iconSize * kIconButtonFactor, + size: iconSize * iconButtonFactor, icon: Icon( iconData, size: iconSize, diff --git a/lib/src/widgets/toolbar/buttons/indent.dart b/lib/src/widgets/toolbar/buttons/indent.dart index 8aad9671..22c3eb29 100644 --- a/lib/src/widgets/toolbar/buttons/indent.dart +++ b/lib/src/widgets/toolbar/buttons/indent.dart @@ -1,15 +1,12 @@ import 'package:flutter/material.dart'; +import '../../../extensions/quill_provider.dart'; +import '../../../l10n/extensions/localizations.dart'; import '../../../models/config/toolbar/buttons/indent.dart'; import '../../../models/themes/quill_icon_theme.dart'; -import '../../../translations/toolbar.i18n.dart'; -import '../../../utils/extensions/build_context.dart'; import '../../controller.dart'; import '../base_toolbar.dart' - show - QuillToolbarBaseButtonOptions, - QuillToolbarIconButton, - kIconButtonFactor; + show QuillToolbarBaseButtonOptions, QuillToolbarIconButton; class QuillToolbarIndentButton extends StatefulWidget { const QuillToolbarIndentButton({ @@ -24,11 +21,11 @@ class QuillToolbarIndentButton extends StatefulWidget { final QuillToolbarIndentButtonOptions options; @override - _QuillToolbarIndentButtonState createState() => - _QuillToolbarIndentButtonState(); + QuillToolbarIndentButtonState createState() => + QuillToolbarIndentButtonState(); } -class _QuillToolbarIndentButtonState extends State { +class QuillToolbarIndentButtonState extends State { QuillToolbarIndentButtonOptions get options { return widget.options; } @@ -43,6 +40,12 @@ class _QuillToolbarIndentButtonState extends State { return iconSize ?? baseFontSize; } + double get iconButtonFactor { + final baseIconFactor = baseButtonExtraOptions.globalIconButtonFactor; + final iconButtonFactor = options.iconButtonFactor; + return iconButtonFactor ?? baseIconFactor; + } + VoidCallback? get afterButtonPressed { return options.afterButtonPressed ?? baseButtonExtraOptions.afterButtonPressed; @@ -67,7 +70,9 @@ class _QuillToolbarIndentButtonState extends State { String get tooltip { return options.tooltip ?? baseButtonExtraOptions.tooltip ?? - (widget.isIncrease ? 'Increase indent'.i18n : 'Decrease indent'.i18n); + (widget.isIncrease + ? context.loc.increaseIndent + : context.loc.decreaseIndent); } void _sharedOnPressed() { @@ -85,6 +90,7 @@ class _QuillToolbarIndentButtonState extends State { afterButtonPressed: afterButtonPressed, iconData: iconData, iconSize: iconSize, + iconButtonFactor: iconButtonFactor, iconTheme: iconTheme, tooltip: tooltip, ), @@ -107,7 +113,7 @@ class _QuillToolbarIndentButtonState extends State { tooltip: tooltip, highlightElevation: 0, hoverElevation: 0, - size: iconSize * kIconButtonFactor, + size: iconSize * iconButtonFactor, icon: Icon(iconData, size: iconSize, color: iconColor), fillColor: iconFillColor, borderRadius: iconTheme?.borderRadius ?? 2, diff --git a/lib/src/widgets/toolbar/buttons/link_style.dart b/lib/src/widgets/toolbar/buttons/link_style.dart index 15cd5af1..c87eb0e9 100644 --- a/lib/src/widgets/toolbar/buttons/link_style.dart +++ b/lib/src/widgets/toolbar/buttons/link_style.dart @@ -1,14 +1,16 @@ import 'package:flutter/material.dart'; +import '../../../extensions/quill_provider.dart'; +import '../../../l10n/extensions/localizations.dart'; +import '../../../l10n/widgets/localizations.dart'; import '../../../models/documents/attribute.dart'; import '../../../models/rules/insert.dart'; import '../../../models/structs/link_dialog_action.dart'; import '../../../models/themes/quill_dialog_theme.dart'; import '../../../models/themes/quill_icon_theme.dart'; -import '../../../translations/toolbar.i18n.dart'; -import '../../../utils/extensions/build_context.dart'; import '../../controller.dart'; import '../../link.dart'; +import '../../utils/provider.dart'; import '../base_toolbar.dart'; class QuillToolbarLinkStyleButton extends StatefulWidget { @@ -22,11 +24,11 @@ class QuillToolbarLinkStyleButton extends StatefulWidget { final QuillToolbarLinkStyleButtonOptions options; @override - _QuillToolbarLinkStyleButtonState createState() => - _QuillToolbarLinkStyleButtonState(); + QuillToolbarLinkStyleButtonState createState() => + QuillToolbarLinkStyleButtonState(); } -class _QuillToolbarLinkStyleButtonState +class QuillToolbarLinkStyleButtonState extends State { void _didChangeSelection() { setState(() {}); @@ -67,6 +69,12 @@ class _QuillToolbarLinkStyleButtonState return iconSize ?? baseFontSize; } + double get iconButtonFactor { + final baseIconFactor = baseButtonExtraOptions.globalIconButtonFactor; + final iconButtonFactor = options.iconButtonFactor; + return iconButtonFactor ?? baseIconFactor; + } + VoidCallback? get afterButtonPressed { return options.afterButtonPressed ?? baseButtonExtraOptions.afterButtonPressed; @@ -83,7 +91,7 @@ class _QuillToolbarLinkStyleButtonState String get tooltip { return options.tooltip ?? baseButtonExtraOptions.tooltip ?? - 'Insert URL'.i18n; + context.loc.insertURL; } IconData get iconData { @@ -95,14 +103,13 @@ class _QuillToolbarLinkStyleButtonState context.requireQuillSharedConfigurations.dialogBarrierColor; } - RegExp get linkRegExp { - return options.linkRegExp ?? RegExp(r'https?://\S+'); + RegExp? get linkRegExp { + return options.linkRegExp; } @override Widget build(BuildContext context) { final isToggled = _getLinkAttributeValue() != null; - final pressedHandler = () => _openLinkDialog(context); final childBuilder = options.childBuilder ?? baseButtonExtraOptions.childBuilder; @@ -115,6 +122,7 @@ class _QuillToolbarLinkStyleButtonState dialogTheme: options.dialogTheme, iconData: iconData, iconSize: iconSize, + iconButtonFactor: iconButtonFactor, tooltip: tooltip, linkDialogAction: options.linkDialogAction, linkRegExp: linkRegExp, @@ -124,7 +132,7 @@ class _QuillToolbarLinkStyleButtonState context: context, controller: controller, onPressed: () { - pressedHandler(); + _openLinkDialog(context); afterButtonPressed?.call(); }, ), @@ -135,7 +143,7 @@ class _QuillToolbarLinkStyleButtonState tooltip: tooltip, highlightElevation: 0, hoverElevation: 0, - size: iconSize * kIconButtonFactor, + size: iconSize * iconButtonFactor, icon: Icon( iconData, size: iconSize, @@ -144,10 +152,10 @@ class _QuillToolbarLinkStyleButtonState : (iconTheme?.iconUnselectedColor ?? theme.iconTheme.color), ), fillColor: isToggled - ? (iconTheme?.iconSelectedFillColor ?? Theme.of(context).primaryColor) + ? (iconTheme?.iconSelectedFillColor ?? theme.primaryColor) : (iconTheme?.iconUnselectedFillColor ?? theme.canvasColor), borderRadius: iconTheme?.borderRadius ?? 2, - onPressed: pressedHandler, + onPressed: () => _openLinkDialog(context), afterPressed: afterButtonPressed, ); } @@ -158,11 +166,11 @@ class _QuillToolbarLinkStyleButtonState final value = await showDialog<_TextLink>( context: context, barrierColor: dialogBarrierColor, - builder: (ctx) { + builder: (_) { final link = _getLinkAttributeValue(); final index = controller.selection.start; - var text; + String? text; if (link != null) { // text should be the link's corresponding text, not selection final leaf = controller.document.querySegmentLeafNode(index).leaf; @@ -173,12 +181,17 @@ class _QuillToolbarLinkStyleButtonState final len = controller.selection.end - index; text ??= len == 0 ? '' : controller.document.getPlainText(index, len); - return _LinkDialog( - dialogTheme: options.dialogTheme, - link: link, - text: text, - linkRegExp: linkRegExp, - action: options.linkDialogAction, + return QuillProvider.value( + value: context.requireQuillProvider, + child: FlutterQuillLocalizationsWidget( + child: _LinkDialog( + dialogTheme: options.dialogTheme, + link: link, + text: text, + linkRegExp: linkRegExp, + action: options.linkDialogAction, + ), + ), ); }, ); @@ -236,7 +249,11 @@ class _LinkDialog extends StatefulWidget { class _LinkDialogState extends State<_LinkDialog> { late String _link; late String _text; - late RegExp linkRegExp; + + RegExp get linkRegExp { + return widget.linkRegExp ?? AutoFormatMultipleLinksRule.oneLineLinkRegExp; + } + late TextEditingController _linkController; late TextEditingController _textController; @@ -245,7 +262,6 @@ class _LinkDialogState extends State<_LinkDialog> { super.initState(); _link = widget.link ?? ''; _text = widget.text ?? ''; - linkRegExp = widget.linkRegExp ?? AutoFormatMultipleLinksRule.oneLineRegExp; _linkController = TextEditingController(text: _link); _textController = TextEditingController(text: _text); } @@ -270,8 +286,8 @@ class _LinkDialogState extends State<_LinkDialog> { keyboardType: TextInputType.text, style: widget.dialogTheme?.inputTextStyle, decoration: InputDecoration( - labelText: 'Text'.i18n, - hintText: 'Please enter a text for your link'.i18n, + labelText: context.loc.text, + hintText: context.loc.pleaseEnterTextForYourLink, labelStyle: widget.dialogTheme?.labelTextStyle, floatingLabelStyle: widget.dialogTheme?.labelTextStyle, ), @@ -279,7 +295,7 @@ class _LinkDialogState extends State<_LinkDialog> { onChanged: _textChanged, controller: _textController, textInputAction: TextInputAction.next, - autofillHints: [ + autofillHints: const [ AutofillHints.name, AutofillHints.url, ], @@ -289,15 +305,15 @@ class _LinkDialogState extends State<_LinkDialog> { keyboardType: TextInputType.url, style: widget.dialogTheme?.inputTextStyle, decoration: InputDecoration( - labelText: 'Link'.i18n, - hintText: 'Please enter the link url'.i18n, + labelText: context.loc.link, + hintText: context.loc.pleaseEnterTheLinkURL, labelStyle: widget.dialogTheme?.labelTextStyle, floatingLabelStyle: widget.dialogTheme?.labelTextStyle, ), onChanged: _linkChanged, controller: _linkController, textInputAction: TextInputAction.done, - autofillHints: [AutofillHints.url], + autofillHints: const [AutofillHints.url], autocorrect: false, onEditingComplete: () { if (!_canPress()) { @@ -326,7 +342,7 @@ class _LinkDialogState extends State<_LinkDialog> { return TextButton( onPressed: _canPress() ? _applyLink : null, child: Text( - 'Ok'.i18n, + context.loc.ok, style: widget.dialogTheme?.buttonTextStyle, ), ); diff --git a/lib/src/widgets/toolbar/buttons/link_style2.dart b/lib/src/widgets/toolbar/buttons/link_style2.dart index d2ecbddd..52d921d4 100644 --- a/lib/src/widgets/toolbar/buttons/link_style2.dart +++ b/lib/src/widgets/toolbar/buttons/link_style2.dart @@ -4,7 +4,9 @@ import 'package:url_launcher/link.dart'; import '../../../../extensions.dart' show UtilityWidgets, AutoFormatMultipleLinksRule; -import '../../../../translations.dart'; +import '../../../extensions/quill_provider.dart'; +import '../../../l10n/extensions/localizations.dart'; +import '../../../models/config/toolbar/buttons/link_style2.dart'; import '../../../models/documents/attribute.dart'; import '../../../models/themes/quill_dialog_theme.dart'; import '../../../models/themes/quill_icon_theme.dart'; @@ -16,60 +18,20 @@ import '../base_toolbar.dart'; /// customization /// and uses dialog similar to one which is used on [http://quilljs.com]. class QuillToolbarLinkStyleButton2 extends StatefulWidget { - const QuillToolbarLinkStyleButton2({ + QuillToolbarLinkStyleButton2({ required this.controller, - this.icon, - this.iconSize = kDefaultIconSize, - this.iconTheme, - this.dialogTheme, - this.afterButtonPressed, - this.tooltip, - this.constraints, - this.addLinkLabel, - this.editLinkLabel, - this.linkColor, - this.childrenSpacing = 16.0, - this.autovalidateMode = AutovalidateMode.disabled, - this.validationMessage, - this.buttonSize, - this.dialogBarrierColor = Colors.black54, - Key? key, - }) : assert(addLinkLabel == null || addLinkLabel.length > 0), - assert(editLinkLabel == null || editLinkLabel.length > 0), - assert(childrenSpacing > 0), - assert(validationMessage == null || validationMessage.length > 0), - super(key: key); + required this.options, + super.key, + }) : assert(options.addLinkLabel == null || + (options.addLinkLabel?.isNotEmpty ?? true)), + assert(options.editLinkLabel == null || + (options.editLinkLabel?.isNotEmpty ?? true)), + assert(options.childrenSpacing > 0), + assert(options.validationMessage == null || + (options.validationMessage?.isNotEmpty ?? true)); final QuillController controller; - final IconData? icon; - final double iconSize; - final QuillIconTheme? iconTheme; - final QuillDialogTheme? dialogTheme; - final VoidCallback? afterButtonPressed; - final String? tooltip; - - /// The constrains for dialog. - final BoxConstraints? constraints; - - /// The text of label in link add mode. - final String? addLinkLabel; - - /// The text of label in link edit mode. - final String? editLinkLabel; - - /// The color of URL. - final Color? linkColor; - - /// The margin between child widgets in the dialog. - final double childrenSpacing; - - final AutovalidateMode autovalidateMode; - final String? validationMessage; - - /// The size of dialog buttons. - final Size? buttonSize; - - final Color dialogBarrierColor; + final QuillToolbarLinkStyleButton2Options options; @override State createState() => @@ -99,30 +61,108 @@ class _QuillToolbarLinkStyleButton2State } } + QuillController get controller { + return widget.controller; + } + + QuillToolbarLinkStyleButton2Options get options { + return widget.options; + } + + double get iconSize { + final baseFontSize = baseButtonExtraOptions.globalIconSize; + final iconSize = options.iconSize; + return iconSize ?? baseFontSize; + } + + double get iconButtonFactor { + final baseIconFactor = baseButtonExtraOptions.globalIconButtonFactor; + final iconButtonFactor = options.iconButtonFactor; + return iconButtonFactor ?? baseIconFactor; + } + + VoidCallback? get afterButtonPressed { + return options.afterButtonPressed ?? + baseButtonExtraOptions.afterButtonPressed; + } + + QuillIconTheme? get iconTheme { + return options.iconTheme ?? baseButtonExtraOptions.iconTheme; + } + + QuillToolbarBaseButtonOptions get baseButtonExtraOptions { + return context.requireQuillToolbarBaseButtonOptions; + } + + String get tooltip { + return options.tooltip ?? + baseButtonExtraOptions.tooltip ?? + context.loc.insertURL; + } + + IconData get iconData { + return options.iconData ?? baseButtonExtraOptions.iconData ?? Icons.link; + } + + Color get dialogBarrierColor { + return options.dialogBarrierColor ?? + context.requireQuillSharedConfigurations.dialogBarrierColor; + } + @override Widget build(BuildContext context) { + final childBuilder = + options.childBuilder ?? baseButtonExtraOptions.childBuilder; + if (childBuilder != null) { + return childBuilder( + QuillToolbarLinkStyleButton2Options( + iconData: iconData, + addLinkLabel: options.addLinkLabel, + afterButtonPressed: options.afterButtonPressed, + autovalidateMode: options.autovalidateMode, + buttonSize: options.buttonSize, + childrenSpacing: options.childrenSpacing, + dialogBarrierColor: dialogBarrierColor, + dialogTheme: options.dialogTheme, + iconSize: iconSize, + iconButtonFactor: iconButtonFactor, + constraints: options.constraints, + tooltip: tooltip, + iconTheme: iconTheme, + editLinkLabel: options.editLinkLabel, + validationMessage: options.validationMessage, + linkColor: options.linkColor, + ), + QuillToolbarLinkStyleButton2ExtraOptions( + controller: controller, + context: context, + onPressed: () { + _openLinkDialog(); + afterButtonPressed?.call(); + }, + ), + ); + } final theme = Theme.of(context); final isToggled = _getLinkAttributeValue() != null; return QuillToolbarIconButton( - tooltip: widget.tooltip, + tooltip: tooltip, highlightElevation: 0, hoverElevation: 0, - size: widget.iconSize * kIconButtonFactor, + size: iconSize * iconButtonFactor, icon: Icon( - widget.icon ?? Icons.link, - size: widget.iconSize, + iconData, + size: iconSize, color: isToggled - ? (widget.iconTheme?.iconSelectedColor ?? - theme.primaryIconTheme.color) - : (widget.iconTheme?.iconUnselectedColor ?? theme.iconTheme.color), + ? (iconTheme?.iconSelectedColor ?? theme.primaryIconTheme.color) + : (iconTheme?.iconUnselectedColor ?? theme.iconTheme.color), ), fillColor: isToggled - ? (widget.iconTheme?.iconSelectedFillColor ?? - Theme.of(context).primaryColor) - : (widget.iconTheme?.iconUnselectedFillColor ?? theme.canvasColor), - borderRadius: widget.iconTheme?.borderRadius ?? 2, + ? (iconTheme?.iconSelectedFillColor ?? theme.primaryColor) + : (iconTheme?.iconUnselectedFillColor ?? theme.canvasColor), + borderRadius: iconTheme?.borderRadius ?? 2, onPressed: _openLinkDialog, - afterPressed: widget.afterButtonPressed, + afterPressed: afterButtonPressed, ); } @@ -131,19 +171,19 @@ class _QuillToolbarLinkStyleButton2State final textLink = await showDialog( context: context, - barrierColor: widget.dialogBarrierColor, + barrierColor: dialogBarrierColor, builder: (_) => LinkStyleDialog( - dialogTheme: widget.dialogTheme, + dialogTheme: options.dialogTheme, text: initialTextLink.text, link: initialTextLink.link, - constraints: widget.constraints, - addLinkLabel: widget.addLinkLabel, - editLinkLabel: widget.editLinkLabel, - linkColor: widget.linkColor, - childrenSpacing: widget.childrenSpacing, - autovalidateMode: widget.autovalidateMode, - validationMessage: widget.validationMessage, - buttonSize: widget.buttonSize, + constraints: options.constraints, + addLinkLabel: options.addLinkLabel, + editLinkLabel: options.editLinkLabel, + linkColor: options.linkColor, + childrenSpacing: options.childrenSpacing, + autovalidateMode: options.autovalidateMode, + validationMessage: options.validationMessage, + buttonSize: options.buttonSize, ), ); @@ -166,7 +206,7 @@ class _QuillToolbarLinkStyleButton2State class LinkStyleDialog extends StatefulWidget { const LinkStyleDialog({ - Key? key, + super.key, this.text, this.link, this.dialogTheme, @@ -183,8 +223,7 @@ class LinkStyleDialog extends StatefulWidget { }) : assert(addLinkLabel == null || addLinkLabel.length > 0), assert(editLinkLabel == null || editLinkLabel.length > 0), assert(childrenSpacing > 0), - assert(validationMessage == null || validationMessage.length > 0), - super(key: key); + assert(validationMessage == null || validationMessage.length > 0); final String? text; final String? link; @@ -269,7 +308,7 @@ class _LinkStyleDialogState extends State { final children = _isEditMode ? [ - Text(widget.editLinkLabel ?? 'Visit link'.i18n), + Text(widget.editLinkLabel ?? context.loc.visitLink), UtilityWidgets.maybeWidget( enabled: !isWrappable, wrapper: (child) => Expanded( @@ -310,19 +349,19 @@ class _LinkStyleDialogState extends State { }); }, style: buttonStyle, - child: Text('Edit'.i18n), + child: Text(context.loc.edit), ), Padding( padding: EdgeInsets.only(left: widget.childrenSpacing), child: ElevatedButton( onPressed: _removeLink, style: buttonStyle, - child: Text('Remove'.i18n), + child: Text(context.loc.remove), ), ), ] : [ - Text(widget.addLinkLabel ?? 'Enter link'.i18n), + Text(widget.addLinkLabel ?? context.loc.enterLink), UtilityWidgets.maybeWidget( enabled: !isWrappable, wrapper: (child) => Expanded( @@ -349,7 +388,7 @@ class _LinkStyleDialogState extends State { ElevatedButton( onPressed: _canPress() ? _applyLink : null, style: buttonStyle, - child: Text('Apply'.i18n), + child: Text(context.loc.apply), ), ]; @@ -387,7 +426,7 @@ class _LinkStyleDialogState extends State { String? _validateLink(String? value) { if ((value?.isEmpty ?? false) || - !AutoFormatMultipleLinksRule.oneLineRegExp.hasMatch(value!)) { + !AutoFormatMultipleLinksRule.oneLineLinkRegExp.hasMatch(value!)) { return widget.validationMessage ?? 'That is not a valid URL'; } @@ -416,7 +455,7 @@ class QuillTextLink { controller.getSelectionStyle().attributes[Attribute.link.key]?.value; final index = controller.selection.start; - var text; + String? text; if (link != null) { // text should be the link's corresponding text, not selection final leaf = controller.document.querySegmentLeafNode(index).leaf; diff --git a/lib/src/widgets/toolbar/buttons/quill_icon.dart b/lib/src/widgets/toolbar/buttons/quill_icon.dart index 0ac6e8f9..d78494d0 100644 --- a/lib/src/widgets/toolbar/buttons/quill_icon.dart +++ b/lib/src/widgets/toolbar/buttons/quill_icon.dart @@ -13,12 +13,13 @@ class QuillToolbarIconButton extends StatelessWidget { this.highlightElevation = 1, this.borderRadius = 2, this.tooltip, - Key? key, - }) : super(key: key); + super.key, + }); final VoidCallback? onPressed; final VoidCallback? afterPressed; final Widget? icon; + final double size; final Color? fillColor; final double hoverElevation; diff --git a/lib/src/widgets/toolbar/buttons/search/search.dart b/lib/src/widgets/toolbar/buttons/search/search.dart index 0f8537f0..f800566b 100644 --- a/lib/src/widgets/toolbar/buttons/search/search.dart +++ b/lib/src/widgets/toolbar/buttons/search/search.dart @@ -1,10 +1,12 @@ import 'package:flutter/material.dart'; -import '../../../../../translations.dart'; +import '../../../../extensions/quill_provider.dart'; +import '../../../../l10n/extensions/localizations.dart'; +import '../../../../l10n/widgets/localizations.dart'; import '../../../../models/themes/quill_dialog_theme.dart'; import '../../../../models/themes/quill_icon_theme.dart'; -import '../../../../utils/extensions/build_context.dart'; import '../../../controller.dart'; +import '../../../utils/provider.dart'; import '../../base_toolbar.dart'; class QuillToolbarSearchButton extends StatelessWidget { @@ -27,6 +29,13 @@ class QuillToolbarSearchButton extends StatelessWidget { return iconSize ?? baseFontSize; } + double _iconButtonFactor(BuildContext context) { + final baseIconFactor = + baseButtonExtraOptions(context).globalIconButtonFactor; + final iconButtonFactor = options.iconButtonFactor; + return iconButtonFactor ?? baseIconFactor; + } + VoidCallback? _afterButtonPressed(BuildContext context) { return options.afterButtonPressed ?? baseButtonExtraOptions(context).afterButtonPressed; @@ -49,7 +58,7 @@ class QuillToolbarSearchButton extends StatelessWidget { String _tooltip(BuildContext context) { return options.tooltip ?? baseButtonExtraOptions(context).tooltip ?? - ('Search'.i18n); + (context.loc.search); } Color _dialogBarrierColor(BuildContext context) { @@ -68,6 +77,7 @@ class QuillToolbarSearchButton extends StatelessWidget { final tooltip = _tooltip(context); final iconData = _iconData(context); final iconSize = _iconSize(context); + final iconButtonFactor = _iconButtonFactor(context); final afterButtonPressed = _afterButtonPressed(context); final childBuilder = @@ -81,10 +91,11 @@ class QuillToolbarSearchButton extends StatelessWidget { dialogBarrierColor: _dialogBarrierColor(context), dialogTheme: _dialogTheme(context), fillColor: options.fillColor, - iconData: _iconData(context), - iconSize: _iconSize(context), - tooltip: _tooltip(context), - iconTheme: _iconTheme(context), + iconData: iconData, + iconSize: iconSize, + iconButtonFactor: iconButtonFactor, + tooltip: tooltip, + iconTheme: iconTheme, ), QuillToolbarSearchButtonExtraOptions( controller: controller, @@ -112,7 +123,7 @@ class QuillToolbarSearchButton extends StatelessWidget { ), highlightElevation: 0, hoverElevation: 0, - size: iconSize * kIconButtonFactor, + size: iconSize * iconButtonFactor, fillColor: iconFillColor, borderRadius: iconTheme?.borderRadius ?? 2, onPressed: () => _sharedOnPressed(context), @@ -131,57 +142,16 @@ class QuillToolbarSearchButton extends StatelessWidget { await showDialog( barrierColor: _dialogBarrierColor(context), context: context, - builder: (_) => QuillToolbarSearchDialog( - controller: controller, - dialogTheme: _dialogTheme(context), - text: '', + builder: (_) => QuillProvider.value( + value: context.requireQuillProvider, + child: FlutterQuillLocalizationsWidget( + child: QuillToolbarSearchDialog( + controller: controller, + dialogTheme: _dialogTheme(context), + text: '', + ), + ), ), ); } - - // Those functions ((findText, moveToPosition)) are not ready yet. - // but consider moving them to a better place - // List _findText({ - // required int index, - // required String text, - // required QuillController controller, - // required List offsets, - // required bool wholeWord, - // required bool caseSensitive, - // bool moveToPosition = true, - // }) { - // if (text.isEmpty) { - // return List.empty(); - // } - // final newOffsets = controller.document.search( - // text, - // caseSensitive: caseSensitive, - // wholeWord: wholeWord, - // ); - // index = 0; // TODO: This might need to be updated... - // if (offsets.isNotEmpty && moveToPosition) { - // _moveToPosition( - // index: index, - // text: text, - // controller: controller, - // offsets: offsets, - // ); - // } - // return newOffsets; - // } - - // void _moveToPosition({ - // required int index, - // required String text, - // required QuillController controller, - // required List offsets, - // }) { - // controller.updateSelection( - // TextSelection( - // baseOffset: offsets[index], - // extentOffset: offsets[index] + text.length, - // ), - // ChangeSource.LOCAL, - // ); - // } } diff --git a/lib/src/widgets/toolbar/buttons/search/search_dialog.dart b/lib/src/widgets/toolbar/buttons/search/search_dialog.dart index dc279ab4..2aa268c5 100644 --- a/lib/src/widgets/toolbar/buttons/search/search_dialog.dart +++ b/lib/src/widgets/toolbar/buttons/search/search_dialog.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; -import '../../../../../translations.dart'; +import '../../../../l10n/extensions/localizations.dart'; import '../../../../models/documents/document.dart'; import '../../../../models/themes/quill_dialog_theme.dart'; import '../../../controller.dart'; @@ -52,11 +52,11 @@ class QuillToolbarSearchDialog extends StatefulWidget { final QuillToolbarSearchDialogChildBuilder? childBuilder; @override - _QuillToolbarSearchDialogState createState() => - _QuillToolbarSearchDialogState(); + QuillToolbarSearchDialogState createState() => + QuillToolbarSearchDialogState(); } -class _QuillToolbarSearchDialogState extends State { +class QuillToolbarSearchDialogState extends State { late String _text; late TextEditingController _controller; late List? _offsets; @@ -121,7 +121,7 @@ class _QuillToolbarSearchDialogState extends State { child: Row( children: [ Tooltip( - message: 'Case sensitivity and whole word search'.i18n, + message: context.loc.caseSensitivityAndWholeWordSearch, child: ToggleButtons( onPressed: (index) { if (index == 0) { @@ -172,19 +172,19 @@ class _QuillToolbarSearchDialogState extends State { if (_offsets == null) IconButton( icon: const Icon(Icons.search), - tooltip: 'Find text'.i18n, + tooltip: context.loc.findText, onPressed: _findText, ), if (_offsets != null) IconButton( icon: const Icon(Icons.keyboard_arrow_up), - tooltip: 'Move to previous occurrence'.i18n, + tooltip: context.loc.moveToPreviousOccurrence, onPressed: (_offsets!.isNotEmpty) ? _moveToPrevious : null, ), if (_offsets != null) IconButton( icon: const Icon(Icons.keyboard_arrow_down), - tooltip: 'Move to next occurrence'.i18n, + tooltip: context.loc.moveToNextOccurrence, onPressed: (_offsets!.isNotEmpty) ? _moveToNext : null, ), ], @@ -217,7 +217,7 @@ class _QuillToolbarSearchDialogState extends State { baseOffset: _offsets![_index], extentOffset: _offsets![_index] + _text.length, ), - ChangeSource.LOCAL, + ChangeSource.local, ); } diff --git a/lib/src/widgets/toolbar/buttons/select_alignment.dart b/lib/src/widgets/toolbar/buttons/select_alignment.dart index 2d4018dd..c81dc1e1 100644 --- a/lib/src/widgets/toolbar/buttons/select_alignment.dart +++ b/lib/src/widgets/toolbar/buttons/select_alignment.dart @@ -1,11 +1,11 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import '../../../../translations.dart'; +import '../../../extensions/quill_provider.dart'; +import '../../../l10n/extensions/localizations.dart'; import '../../../models/documents/attribute.dart'; import '../../../models/documents/style.dart'; import '../../../models/themes/quill_icon_theme.dart'; -import '../../../utils/extensions/build_context.dart'; import '../../../utils/widgets.dart'; import '../../controller.dart'; import '../base_toolbar.dart'; @@ -32,11 +32,11 @@ class QuillToolbarSelectAlignmentButton extends StatefulWidget { final EdgeInsetsGeometry? padding; @override - _QuillToolbarSelectAlignmentButtonState createState() => - _QuillToolbarSelectAlignmentButtonState(); + QuillToolbarSelectAlignmentButtonState createState() => + QuillToolbarSelectAlignmentButtonState(); } -class _QuillToolbarSelectAlignmentButtonState +class QuillToolbarSelectAlignmentButtonState extends State { Attribute? _value; @@ -60,18 +60,24 @@ class _QuillToolbarSelectAlignmentButtonState return widget.controller; } - double get iconSize { + double get _iconSize { final baseFontSize = baseButtonExtraOptions.globalIconSize; final iconSize = options.iconSize; return iconSize ?? baseFontSize; } - VoidCallback? get afterButtonPressed { + double get _iconButtonFactor { + final baseIconFactor = baseButtonExtraOptions.globalIconButtonFactor; + final iconButtonFactor = options.iconButtonFactor; + return iconButtonFactor ?? baseIconFactor; + } + + VoidCallback? get _afterButtonPressed { return options.afterButtonPressed ?? baseButtonExtraOptions.afterButtonPressed; } - QuillIconTheme? get iconTheme { + QuillIconTheme? get _iconTheme { return options.iconTheme ?? baseButtonExtraOptions.iconTheme; } @@ -116,10 +122,10 @@ class _QuillToolbarSelectAlignmentButtonState ); } return QuillSelectAlignmentValues( - leftAlignment: 'Align left'.i18n, - centerAlignment: 'Align center'.i18n, - rightAlignment: 'Align right'.i18n, - justifyAlignment: 'Justify win width'.i18n, + leftAlignment: context.loc.alignLeft, + centerAlignment: context.loc.alignCenter, + rightAlignment: context.loc.alignRight, + justifyAlignment: context.loc.justifyWinWidth, ); } @@ -149,7 +155,7 @@ class _QuillToolbarSelectAlignmentButtonState @override Widget build(BuildContext context) { - final _valueToText = { + final valueToText = { if (widget.showLeftAlignment!) Attribute.leftAlignment: Attribute.leftAlignment.value!, if (widget.showCenterAlignment!) @@ -160,13 +166,13 @@ class _QuillToolbarSelectAlignmentButtonState Attribute.justifyAlignment: Attribute.justifyAlignment.value!, }; - final _valueAttribute = [ + final valueAttribute = [ if (widget.showLeftAlignment!) Attribute.leftAlignment, if (widget.showCenterAlignment!) Attribute.centerAlignment, if (widget.showRightAlignment!) Attribute.rightAlignment, if (widget.showJustifyAlignment!) Attribute.justifyAlignment ]; - final _valueString = [ + final valueString = [ if (widget.showLeftAlignment!) Attribute.leftAlignment.value!, if (widget.showCenterAlignment!) Attribute.centerAlignment.value!, if (widget.showRightAlignment!) Attribute.rightAlignment.value!, @@ -191,32 +197,50 @@ class _QuillToolbarSelectAlignmentButtonState final childBuilder = options.childBuilder ?? baseButtonExtraOptions.childBuilder; - if (childBuilder != null) { - throw UnsupportedError( - 'Sorry but the `childBuilder` for the Select alignment buttons' - ' is not supported. Yet but we will work on that soon.', - ); + void sharedOnPressed(int index) { + valueAttribute[index] == Attribute.leftAlignment + ? controller.formatSelection( + Attribute.clone(Attribute.align, null), + ) + : controller.formatSelection(valueAttribute[index]); + _afterButtonPressed?.call(); } - final theme = Theme.of(context); - return Row( mainAxisSize: MainAxisSize.min, children: List.generate(buttonCount, (index) { + if (childBuilder != null) { + return childBuilder( + QuillToolbarSelectAlignmentButtonOptions( + afterButtonPressed: _afterButtonPressed, + iconSize: _iconSize, + iconButtonFactor: _iconButtonFactor, + iconTheme: _iconTheme, + tooltips: _tooltips, + iconsData: _iconsData, + ), + QuillToolbarSelectAlignmentButtonExtraOptions( + context: context, + controller: controller, + onPressed: () => sharedOnPressed(index), + ), + ); + } + final theme = Theme.of(context); return Padding( padding: widget.padding ?? const EdgeInsets.symmetric(horizontal: !kIsWeb ? 1.0 : 5.0), child: ConstrainedBox( constraints: BoxConstraints.tightFor( - width: iconSize * kIconButtonFactor, - height: iconSize * kIconButtonFactor, + width: _iconSize * _iconButtonFactor, + height: _iconSize * _iconButtonFactor, ), child: UtilityWidgets.maybeTooltip( - message: _valueString[index] == Attribute.leftAlignment.value + message: valueString[index] == Attribute.leftAlignment.value ? _tooltips.leftAlignment - : _valueString[index] == Attribute.centerAlignment.value + : valueString[index] == Attribute.centerAlignment.value ? _tooltips.centerAlignment - : _valueString[index] == Attribute.rightAlignment.value + : valueString[index] == Attribute.rightAlignment.value ? _tooltips.rightAlignment : _tooltips.justifyAlignment, child: RawMaterialButton( @@ -226,33 +250,25 @@ class _QuillToolbarSelectAlignmentButtonState visualDensity: VisualDensity.compact, shape: RoundedRectangleBorder( borderRadius: - BorderRadius.circular(iconTheme?.borderRadius ?? 2)), - fillColor: _valueToText[_value] == _valueString[index] - ? (iconTheme?.iconSelectedFillColor ?? - Theme.of(context).primaryColor) - : (iconTheme?.iconUnselectedFillColor ?? theme.canvasColor), - onPressed: () { - _valueAttribute[index] == Attribute.leftAlignment - ? controller.formatSelection( - Attribute.clone(Attribute.align, null), - ) - : controller.formatSelection(_valueAttribute[index]); - afterButtonPressed?.call(); - }, + BorderRadius.circular(_iconTheme?.borderRadius ?? 2)), + fillColor: valueToText[_value] == valueString[index] + ? (_iconTheme?.iconSelectedFillColor ?? theme.primaryColor) + : (_iconTheme?.iconUnselectedFillColor ?? + theme.canvasColor), + onPressed: () => sharedOnPressed(index), child: Icon( - _valueString[index] == Attribute.leftAlignment.value + valueString[index] == Attribute.leftAlignment.value ? _iconsData.leftAlignment - : _valueString[index] == Attribute.centerAlignment.value + : valueString[index] == Attribute.centerAlignment.value ? _iconsData.centerAlignment - : _valueString[index] == - Attribute.rightAlignment.value + : valueString[index] == Attribute.rightAlignment.value ? _iconsData.rightAlignment : _iconsData.justifyAlignment, - size: iconSize, - color: _valueToText[_value] == _valueString[index] - ? (iconTheme?.iconSelectedColor ?? + size: _iconSize, + color: valueToText[_value] == valueString[index] + ? (_iconTheme?.iconSelectedColor ?? theme.primaryIconTheme.color) - : (iconTheme?.iconUnselectedColor ?? + : (_iconTheme?.iconUnselectedColor ?? theme.iconTheme.color), ), ), diff --git a/lib/src/widgets/toolbar/buttons/select_header_style.dart b/lib/src/widgets/toolbar/buttons/select_header_style.dart index 57ba28d2..15e02dab 100644 --- a/lib/src/widgets/toolbar/buttons/select_header_style.dart +++ b/lib/src/widgets/toolbar/buttons/select_header_style.dart @@ -1,11 +1,12 @@ +import 'package:flutter/foundation.dart' show kIsWeb; import 'package:flutter/material.dart'; import '../../../../extensions.dart'; -import '../../../../translations.dart'; +import '../../../extensions/quill_provider.dart'; +import '../../../l10n/extensions/localizations.dart'; import '../../../models/documents/attribute.dart'; import '../../../models/documents/style.dart'; import '../../../models/themes/quill_icon_theme.dart'; -import '../../../utils/extensions/build_context.dart'; import '../../controller.dart'; import '../base_toolbar.dart'; @@ -20,11 +21,11 @@ class QuillToolbarSelectHeaderStyleButtons extends StatefulWidget { final QuillToolbarSelectHeaderStyleButtonsOptions options; @override - _QuillToolbarSelectHeaderStyleButtonsState createState() => - _QuillToolbarSelectHeaderStyleButtonsState(); + QuillToolbarSelectHeaderStyleButtonsState createState() => + QuillToolbarSelectHeaderStyleButtonsState(); } -class _QuillToolbarSelectHeaderStyleButtonsState +class QuillToolbarSelectHeaderStyleButtonsState extends State { Attribute? _selectedAttribute; @@ -60,6 +61,12 @@ class _QuillToolbarSelectHeaderStyleButtonsState return iconSize ?? baseFontSize; } + double get iconButtonFactor { + final baseIconFactor = baseButtonExtraOptions.globalIconButtonFactor; + final iconButtonFactor = options.iconButtonFactor; + return iconButtonFactor ?? baseIconFactor; + } + VoidCallback? get afterButtonPressed { return options.afterButtonPressed ?? baseButtonExtraOptions.afterButtonPressed; @@ -76,7 +83,7 @@ class _QuillToolbarSelectHeaderStyleButtonsState String get tooltip { return options.tooltip ?? baseButtonExtraOptions.tooltip ?? - 'Header style'.i18n; + context.loc.headerStyle; } Axis get axis { @@ -84,16 +91,26 @@ class _QuillToolbarSelectHeaderStyleButtonsState } void _sharedOnPressed(Attribute attribute) { - final _attribute = + final attribute0 = _selectedAttribute == attribute ? Attribute.header : attribute; - controller.formatSelection(_attribute); + controller.formatSelection(attribute0); afterButtonPressed?.call(); } + List get _attrbuites { + return options.attributes ?? + const [ + Attribute.header, + Attribute.h1, + Attribute.h2, + Attribute.h3, + ]; + } + @override Widget build(BuildContext context) { assert( - options.attributes.every( + _attrbuites.every( (element) => _valueToText.keys.contains(element), ), 'All attributes must be one of them: header, h1, h2 or h3', @@ -107,23 +124,33 @@ class _QuillToolbarSelectHeaderStyleButtonsState final childBuilder = options.childBuilder ?? baseButtonExtraOptions.childBuilder; - if (childBuilder != null) { - throw UnsupportedError( - 'Sorry but the `childBuilder` for the Select header button' - ' is not supported. Yet but we will work on that soon.', - ); - } - final theme = Theme.of(context); - - final children = options.attributes.map((attribute) { + final children = _attrbuites.map((attribute) { + if (childBuilder != null) { + return childBuilder( + QuillToolbarSelectHeaderStyleButtonsOptions( + afterButtonPressed: afterButtonPressed, + attributes: _attrbuites, + axis: axis, + iconSize: iconSize, + iconButtonFactor: iconButtonFactor, + iconTheme: iconTheme, + tooltip: tooltip, + ), + QuillToolbarSelectHeaderStyleButtonExtraOptions( + controller: controller, + context: context, + onPressed: () => _sharedOnPressed(attribute), + ), + ); + } + final theme = Theme.of(context); final isSelected = _selectedAttribute == attribute; return Padding( - // Do we really need to ignore (prefer_const_constructors)?? - padding: EdgeInsets.symmetric(horizontal: !isWeb() ? 1.0 : 5.0), + padding: const EdgeInsets.symmetric(horizontal: !kIsWeb ? 1.0 : 5.0), child: ConstrainedBox( constraints: BoxConstraints.tightFor( - width: iconSize * kIconButtonFactor, - height: iconSize * kIconButtonFactor, + width: iconSize * iconButtonFactor, + height: iconSize * iconButtonFactor, ), child: UtilityWidgets.maybeTooltip( message: tooltip, @@ -136,12 +163,14 @@ class _QuillToolbarSelectHeaderStyleButtonsState borderRadius: BorderRadius.circular(iconTheme?.borderRadius ?? 2)), fillColor: isSelected - ? (iconTheme?.iconSelectedFillColor ?? - Theme.of(context).primaryColor) + ? (iconTheme?.iconSelectedFillColor ?? theme.primaryColor) : (iconTheme?.iconUnselectedFillColor ?? theme.canvasColor), onPressed: () => _sharedOnPressed(attribute), child: Text( - _valueToText[attribute] ?? '', + _valueToText[attribute] ?? + (throw ArgumentError.notNull( + 'attrbuite', + )), style: style.copyWith( color: isSelected ? (iconTheme?.iconSelectedColor ?? diff --git a/lib/src/widgets/toolbar/buttons/toggle_check_list.dart b/lib/src/widgets/toolbar/buttons/toggle_check_list.dart index 189b9a30..52ee2f1d 100644 --- a/lib/src/widgets/toolbar/buttons/toggle_check_list.dart +++ b/lib/src/widgets/toolbar/buttons/toggle_check_list.dart @@ -1,12 +1,12 @@ import 'package:flutter/material.dart'; -import '../../../../translations.dart'; +import '../../../extensions/quill_provider.dart'; +import '../../../l10n/extensions/localizations.dart'; import '../../../models/config/toolbar/buttons/base.dart'; import '../../../models/config/toolbar/buttons/toggle_check_list.dart'; import '../../../models/documents/attribute.dart'; import '../../../models/documents/style.dart'; import '../../../models/themes/quill_icon_theme.dart'; -import '../../../utils/extensions/build_context.dart'; import '../../../utils/widgets.dart'; import '../../controller.dart'; import 'toggle_style.dart'; @@ -23,11 +23,11 @@ class QuillToolbarToggleCheckListButton extends StatefulWidget { final QuillController controller; @override - _QuillToolbarToggleCheckListButtonState createState() => - _QuillToolbarToggleCheckListButtonState(); + QuillToolbarToggleCheckListButtonState createState() => + QuillToolbarToggleCheckListButtonState(); } -class _QuillToolbarToggleCheckListButtonState +class QuillToolbarToggleCheckListButtonState extends State { bool? _isToggled; @@ -93,6 +93,12 @@ class _QuillToolbarToggleCheckListButtonState return iconSize ?? baseFontSize; } + double get iconButtonFactor { + final baseIconFactor = baseButtonExtraOptions.globalIconButtonFactor; + final iconButtonFactor = options.iconButtonFactor; + return iconButtonFactor ?? baseIconFactor; + } + VoidCallback? get afterButtonPressed { return options.afterButtonPressed ?? baseButtonExtraOptions.afterButtonPressed; @@ -115,7 +121,7 @@ class _QuillToolbarToggleCheckListButtonState String get tooltip { return options.tooltip ?? baseButtonExtraOptions.tooltip ?? - 'Checked list'.i18n; + context.loc.checkedList; } @override @@ -129,6 +135,7 @@ class _QuillToolbarToggleCheckListButtonState iconTheme: iconTheme, controller: controller, iconSize: iconSize, + iconButtonFactor: iconButtonFactor, tooltip: tooltip, iconData: iconData, ), @@ -154,6 +161,7 @@ class _QuillToolbarToggleCheckListButtonState _toggleAttribute, afterButtonPressed, iconSize, + iconButtonFactor, iconTheme, ), ); diff --git a/lib/src/widgets/toolbar/buttons/toggle_style.dart b/lib/src/widgets/toolbar/buttons/toggle_style.dart index 38b8bc6b..7ac76798 100644 --- a/lib/src/widgets/toolbar/buttons/toggle_style.dart +++ b/lib/src/widgets/toolbar/buttons/toggle_style.dart @@ -1,10 +1,10 @@ import 'package:flutter/material.dart'; -import '../../../../translations.dart'; +import '../../../extensions/quill_provider.dart'; +import '../../../l10n/extensions/localizations.dart'; import '../../../models/documents/attribute.dart'; import '../../../models/documents/style.dart'; import '../../../models/themes/quill_icon_theme.dart'; -import '../../../utils/extensions/build_context.dart'; import '../../../utils/widgets.dart'; import '../../controller.dart'; import '../base_toolbar.dart'; @@ -36,11 +36,11 @@ class QuillToolbarToggleStyleButton extends StatefulWidget { final QuillController controller; @override - _QuillToolbarToggleStyleButtonState createState() => - _QuillToolbarToggleStyleButtonState(); + QuillToolbarToggleStyleButtonState createState() => + QuillToolbarToggleStyleButtonState(); } -class _QuillToolbarToggleStyleButtonState +class QuillToolbarToggleStyleButtonState extends State { bool? _isToggled; @@ -68,6 +68,13 @@ class _QuillToolbarToggleStyleButtonState return iconSize ?? baseFontSize; } + double get iconButtonFactor { + final baseIconFactor = + context.requireQuillToolbarBaseButtonOptions.globalIconButtonFactor; + final iconButtonFactor = options.iconButtonFactor; + return iconButtonFactor ?? baseIconFactor; + } + VoidCallback? get afterButtonPressed { return options.afterButtonPressed ?? context.requireQuillToolbarBaseButtonOptions.afterButtonPressed; @@ -78,36 +85,36 @@ class _QuillToolbarToggleStyleButtonState context.requireQuillToolbarBaseButtonOptions.iconTheme; } - String? get _defaultTooltip { + (String?, IconData) get _defaultTooltipAndIconData { switch (widget.attribute.key) { case 'bold': - return 'Bold'.i18n; + return (context.loc.bold, Icons.format_bold); case 'script': if (widget.attribute.value == ScriptAttributes.sub.value) { - return 'Subscript'.i18n; + return (context.loc.subscript, Icons.subscript); } - return 'Superscript'.i18n; + return (context.loc.superscript, Icons.superscript); case 'italic': - return 'Italic'.i18n; + return (context.loc.italic, Icons.format_italic); case 'small': - return 'Small'.i18n; + return (context.loc.small, Icons.format_size); case 'underline': - return 'Underline'.i18n; + return (context.loc.underline, Icons.format_underline); case 'strike': - return 'Strike through'.i18n; + return (context.loc.strikeThrough, Icons.format_strikethrough); case 'code': - return 'Inline code'.i18n; - case 'rtl': - return 'Text direction'.i18n; + return (context.loc.inlineCode, Icons.code); + case 'direction': + return (context.loc.textDirection, Icons.format_textdirection_r_to_l); case 'list': if (widget.attribute.value == 'bullet') { - return 'Bullet list'.i18n; + return (context.loc.bulletList, Icons.format_list_bulleted); } - return 'Numbered list'.i18n; + return (context.loc.numberedList, Icons.format_list_numbered); case 'code-block': - return 'Code block'.i18n; + return (context.loc.codeBlock, Icons.code); case 'blockquote': - return 'Quote'.i18n; + return (context.loc.quote, Icons.format_quote); default: throw ArgumentError( 'Could not find the default tooltip for ' @@ -119,50 +126,13 @@ class _QuillToolbarToggleStyleButtonState String? get tooltip { return options.tooltip ?? context.requireQuillToolbarBaseButtonOptions.tooltip ?? - _defaultTooltip; - } - - IconData get _defaultIconData { - switch (widget.attribute.key) { - case 'bold': - return Icons.format_bold; - case 'script': - if (widget.attribute.value == ScriptAttributes.sub.value) { - return Icons.subscript; - } - return Icons.superscript; - case 'italic': - return Icons.format_italic; - case 'small': - return Icons.format_size; - case 'underline': - return Icons.format_underline; - case 'strike': - return Icons.format_strikethrough; - case 'code': - return Icons.code; - case 'rtl': - return Icons.format_textdirection_r_to_l; - case 'list': - if (widget.attribute.value == 'bullet') { - return Icons.format_list_bulleted; - } - return Icons.format_list_numbered; - case 'code-block': - return Icons.code; - case 'blockquote': - return Icons.format_quote; - default: - throw ArgumentError( - 'Could not find the icon for ${widget.attribute.toString()}', - ); - } + _defaultTooltipAndIconData.$1; } IconData get iconData { return options.iconData ?? context.requireQuillToolbarBaseButtonOptions.iconData ?? - _defaultIconData; + _defaultTooltipAndIconData.$2; } void _onPressed() { @@ -196,6 +166,7 @@ class _QuillToolbarToggleStyleButtonState _toggleAttribute, options.afterButtonPressed, iconSize, + iconButtonFactor, iconTheme, ), ); @@ -249,6 +220,7 @@ Widget defaultToggleStyleButtonBuilder( VoidCallback? onPressed, VoidCallback? afterPressed, [ double iconSize = kDefaultIconSize, + double iconButtonFactor = kIconButtonFactor, QuillIconTheme? iconTheme, ]) { final theme = Theme.of(context); @@ -271,7 +243,7 @@ Widget defaultToggleStyleButtonBuilder( return QuillToolbarIconButton( highlightElevation: 0, hoverElevation: 0, - size: iconSize * kIconButtonFactor, + size: iconSize * iconButtonFactor, icon: Icon(icon, size: iconSize, color: iconColor), fillColor: fill, onPressed: onPressed, diff --git a/lib/src/widgets/toolbar/toolbar.dart b/lib/src/widgets/toolbar/toolbar.dart index 092f5012..d729b032 100644 --- a/lib/src/widgets/toolbar/toolbar.dart +++ b/lib/src/widgets/toolbar/toolbar.dart @@ -1,8 +1,13 @@ import 'package:flutter/material.dart'; -import '../../../flutter_quill.dart'; +import '../../extensions/quill_provider.dart'; +import '../../l10n/extensions/localizations.dart'; +import '../../models/config/toolbar/base_configurations.dart'; +import '../../models/documents/attribute.dart'; +import '../utils/provider.dart'; +import 'base_toolbar.dart'; -class QuillToolbar extends StatelessWidget { +class QuillToolbar extends StatelessWidget implements PreferredSizeWidget { const QuillToolbar({ super.key, this.configurations = const QuillToolbarConfigurations(), @@ -59,8 +64,6 @@ class QuillToolbar extends StatelessWidget { sectionDividerSpace: configurations.sectionDividerSpace, toolbarSize: configurations.toolbarSize, childrenBuilder: (context) { - final controller = context.requireQuillController; - final toolbarConfigurations = context.requireQuillToolbarConfigurations; @@ -70,36 +73,49 @@ class QuillToolbar extends StatelessWidget { final axis = toolbarConfigurations.axis; final globalController = context.requireQuillController; + final spacerWidget = + configurations.spacerWidget ?? const SizedBox.shrink(); + return [ - if (configurations.showUndo) + if (configurations.showUndo) ...[ QuillToolbarHistoryButton( options: toolbarConfigurations.buttonOptions.undoHistory, controller: toolbarConfigurations .buttonOptions.undoHistory.controller ?? globalController, ), - if (configurations.showRedo) + spacerWidget, + ], + if (configurations.showRedo) ...[ QuillToolbarHistoryButton( options: toolbarConfigurations.buttonOptions.redoHistory, controller: toolbarConfigurations .buttonOptions.redoHistory.controller ?? globalController, ), - if (configurations.showFontFamily) + spacerWidget, + ], + if (configurations.showFontFamily) ...[ QuillToolbarFontFamilyButton( options: toolbarConfigurations.buttonOptions.fontFamily, controller: toolbarConfigurations .buttonOptions.fontFamily.controller ?? globalController, + defaultDispalyText: context.loc.font, ), - if (configurations.showFontSize) + spacerWidget, + ], + if (configurations.showFontSize) ...[ QuillToolbarFontSizeButton( options: toolbarConfigurations.buttonOptions.fontSize, controller: toolbarConfigurations .buttonOptions.fontFamily.controller ?? globalController, + defaultDisplayText: context.loc.fontSize, ), - if (configurations.showBoldButton) + spacerWidget, + ], + if (configurations.showBoldButton) ...[ QuillToolbarToggleStyleButton( attribute: Attribute.bold, options: toolbarConfigurations.buttonOptions.bold, @@ -107,7 +123,9 @@ class QuillToolbar extends StatelessWidget { toolbarConfigurations.buttonOptions.bold.controller ?? globalController, ), - if (configurations.showSubscript) + spacerWidget, + ], + if (configurations.showSubscript) ...[ QuillToolbarToggleStyleButton( attribute: Attribute.subscript, options: toolbarConfigurations.buttonOptions.subscript, @@ -115,7 +133,9 @@ class QuillToolbar extends StatelessWidget { .buttonOptions.subscript.controller ?? globalController, ), - if (configurations.showSuperscript) + spacerWidget, + ], + if (configurations.showSuperscript) ...[ QuillToolbarToggleStyleButton( attribute: Attribute.superscript, options: toolbarConfigurations.buttonOptions.superscript, @@ -123,7 +143,9 @@ class QuillToolbar extends StatelessWidget { .buttonOptions.superscript.controller ?? globalController, ), - if (configurations.showItalicButton) + spacerWidget, + ], + if (configurations.showItalicButton) ...[ QuillToolbarToggleStyleButton( attribute: Attribute.italic, options: toolbarConfigurations.buttonOptions.italic, @@ -131,7 +153,9 @@ class QuillToolbar extends StatelessWidget { toolbarConfigurations.buttonOptions.italic.controller ?? globalController, ), - if (configurations.showSmallButton) + spacerWidget, + ], + if (configurations.showSmallButton) ...[ QuillToolbarToggleStyleButton( attribute: Attribute.small, options: toolbarConfigurations.buttonOptions.small, @@ -139,7 +163,9 @@ class QuillToolbar extends StatelessWidget { toolbarConfigurations.buttonOptions.small.controller ?? globalController, ), - if (configurations.showUnderLineButton) + spacerWidget, + ], + if (configurations.showUnderLineButton) ...[ QuillToolbarToggleStyleButton( attribute: Attribute.underline, options: toolbarConfigurations.buttonOptions.underLine, @@ -147,7 +173,9 @@ class QuillToolbar extends StatelessWidget { .buttonOptions.underLine.controller ?? globalController, ), - if (configurations.showStrikeThrough) + spacerWidget, + ], + if (configurations.showStrikeThrough) ...[ QuillToolbarToggleStyleButton( attribute: Attribute.strikeThrough, options: toolbarConfigurations.buttonOptions.strikeThrough, @@ -155,7 +183,9 @@ class QuillToolbar extends StatelessWidget { .buttonOptions.strikeThrough.controller ?? globalController, ), - if (configurations.showInlineCode) + spacerWidget, + ], + if (configurations.showInlineCode) ...[ QuillToolbarToggleStyleButton( attribute: Attribute.inlineCode, options: toolbarConfigurations.buttonOptions.inlineCode, @@ -163,27 +193,41 @@ class QuillToolbar extends StatelessWidget { .buttonOptions.inlineCode.controller ?? globalController, ), - if (configurations.showColorButton) + spacerWidget, + ], + if (configurations.showColorButton) ...[ QuillToolbarColorButton( - controller: controller, + controller: + toolbarConfigurations.buttonOptions.color.controller ?? + globalController, isBackground: false, options: toolbarConfigurations.buttonOptions.color, ), - if (configurations.showBackgroundColorButton) + spacerWidget, + ], + if (configurations.showBackgroundColorButton) ...[ QuillToolbarColorButton( options: toolbarConfigurations.buttonOptions.backgroundColor, - controller: controller, + controller: + toolbarConfigurations.buttonOptions.color.controller ?? + globalController, isBackground: true, ), - if (configurations.showClearFormat) + spacerWidget, + ], + if (configurations.showClearFormat) ...[ QuillToolbarClearFormatButton( - controller: controller, + controller: toolbarConfigurations + .buttonOptions.clearFormat.controller ?? + globalController, options: toolbarConfigurations.buttonOptions.clearFormat, ), + spacerWidget, + ], if (theEmbedButtons != null) for (final builder in theEmbedButtons) builder( - controller, + globalController, globalIconSize, context.requireQuillToolbarBaseButtonOptions.iconTheme, configurations.dialogTheme), @@ -199,9 +243,11 @@ class QuillToolbar extends StatelessWidget { color: configurations.sectionDividerColor, space: configurations.sectionDividerSpace, ), - if (configurations.showAlignmentButtons) + if (configurations.showAlignmentButtons) ...[ QuillToolbarSelectAlignmentButton( - controller: controller, + controller: toolbarConfigurations + .buttonOptions.selectAlignmentButtons.controller ?? + globalController, options: toolbarConfigurations .buttonOptions.selectAlignmentButtons, // tooltips: Map.of(buttonTooltips) @@ -216,7 +262,9 @@ class QuillToolbar extends StatelessWidget { showRightAlignment: configurations.showRightAlignment, showJustifyAlignment: configurations.showJustifyAlignment, ), - if (configurations.showDirection) + spacerWidget, + ], + if (configurations.showDirection) ...[ QuillToolbarToggleStyleButton( attribute: Attribute.rtl, options: toolbarConfigurations.buttonOptions.direction, @@ -224,6 +272,8 @@ class QuillToolbar extends StatelessWidget { .buttonOptions.direction.controller ?? context.requireQuillController, ), + spacerWidget, + ], if (configurations.showDividers && isButtonGroupShown[1] && (isButtonGroupShown[2] || @@ -235,12 +285,16 @@ class QuillToolbar extends StatelessWidget { color: configurations.sectionDividerColor, space: configurations.sectionDividerSpace, ), - if (configurations.showHeaderStyle) + if (configurations.showHeaderStyle) ...[ QuillToolbarSelectHeaderStyleButtons( - controller: controller, + controller: toolbarConfigurations + .buttonOptions.selectHeaderStyleButtons.controller ?? + globalController, options: toolbarConfigurations .buttonOptions.selectHeaderStyleButtons, ), + spacerWidget, + ], if (configurations.showDividers && configurations.showHeaderStyle && isButtonGroupShown[2] && @@ -252,7 +306,7 @@ class QuillToolbar extends StatelessWidget { color: configurations.sectionDividerColor, space: configurations.sectionDividerSpace, ), - if (configurations.showListNumbers) + if (configurations.showListNumbers) ...[ QuillToolbarToggleStyleButton( attribute: Attribute.ol, options: toolbarConfigurations.buttonOptions.listNumbers, @@ -260,7 +314,9 @@ class QuillToolbar extends StatelessWidget { .buttonOptions.listNumbers.controller ?? globalController, ), - if (configurations.showListBullets) + spacerWidget, + ], + if (configurations.showListBullets) ...[ QuillToolbarToggleStyleButton( attribute: Attribute.ul, options: toolbarConfigurations.buttonOptions.listBullets, @@ -268,14 +324,18 @@ class QuillToolbar extends StatelessWidget { .buttonOptions.listBullets.controller ?? globalController, ), - if (configurations.showListCheck) + spacerWidget, + ], + if (configurations.showListCheck) ...[ QuillToolbarToggleCheckListButton( options: toolbarConfigurations.buttonOptions.toggleCheckList, controller: toolbarConfigurations .buttonOptions.toggleCheckList.controller ?? globalController, ), - if (configurations.showCodeBlock) + spacerWidget, + ], + if (configurations.showCodeBlock) ...[ QuillToolbarToggleStyleButton( attribute: Attribute.codeBlock, options: toolbarConfigurations.buttonOptions.codeBlock, @@ -283,15 +343,18 @@ class QuillToolbar extends StatelessWidget { .buttonOptions.codeBlock.controller ?? globalController, ), + spacerWidget, + ], if (configurations.showDividers && isButtonGroupShown[3] && - (isButtonGroupShown[4] || isButtonGroupShown[5])) + (isButtonGroupShown[4] || isButtonGroupShown[5])) ...[ QuillToolbarDivider( axis, color: configurations.sectionDividerColor, space: configurations.sectionDividerSpace, ), - if (configurations.showQuote) + ], + if (configurations.showQuote) ...[ QuillToolbarToggleStyleButton( options: toolbarConfigurations.buttonOptions.quote, controller: @@ -299,7 +362,9 @@ class QuillToolbar extends StatelessWidget { globalController, attribute: Attribute.blockQuote, ), - if (configurations.showIndent) + spacerWidget, + ], + if (configurations.showIndent) ...[ QuillToolbarIndentButton( controller: toolbarConfigurations .buttonOptions.indentIncrease.controller ?? @@ -307,7 +372,9 @@ class QuillToolbar extends StatelessWidget { isIncrease: true, options: toolbarConfigurations.buttonOptions.indentIncrease, ), - if (configurations.showIndent) + spacerWidget, + ], + if (configurations.showIndent) ...[ QuillToolbarIndentButton( controller: toolbarConfigurations .buttonOptions.indentDecrease.controller ?? @@ -315,6 +382,8 @@ class QuillToolbar extends StatelessWidget { isIncrease: false, options: toolbarConfigurations.buttonOptions.indentDecrease, ), + spacerWidget, + ], if (configurations.showDividers && isButtonGroupShown[4] && isButtonGroupShown[5]) @@ -323,49 +392,61 @@ class QuillToolbar extends StatelessWidget { color: configurations.sectionDividerColor, space: configurations.sectionDividerSpace, ), - if (configurations.showLink) + if (configurations.showLink) ...[ QuillToolbarLinkStyleButton( - controller: controller, + controller: toolbarConfigurations + .buttonOptions.linkStyle.controller ?? + globalController, options: toolbarConfigurations.buttonOptions.linkStyle, ), - if (configurations.showSearchButton) + spacerWidget, + ], + if (configurations.showSearchButton) ...[ QuillToolbarSearchButton( - controller: controller, + controller: + toolbarConfigurations.buttonOptions.search.controller ?? + globalController, options: toolbarConfigurations.buttonOptions.search, ), - if (configurations.customButtons.isNotEmpty) + spacerWidget, + ], + if (configurations.customButtons.isNotEmpty) ...[ if (configurations.showDividers) QuillToolbarDivider( axis, color: configurations.sectionDividerColor, space: configurations.sectionDividerSpace, ), - for (final customButton in configurations.customButtons) - if (customButton.child != null) ...[ - InkWell( - onTap: customButton.onTap, - child: customButton.child, - ), - ] else ...[ - CustomButton( - onPressed: customButton.onTap, - icon: customButton.iconData ?? - context.quillToolbarBaseButtonOptions?.iconData, - iconColor: customButton.iconColor, - iconSize: customButton.iconSize ?? globalIconSize, - iconTheme: - context.requireQuillToolbarBaseButtonOptions.iconTheme, - afterButtonPressed: customButton.afterButtonPressed ?? - context - .quillToolbarBaseButtonOptions?.afterButtonPressed, - tooltip: customButton.tooltip ?? - context.quillToolbarBaseButtonOptions?.tooltip, + for (final customButton in configurations.customButtons) + QuillToolbarCustomButton( + options: customButton, + controller: customButton.controller ?? globalController, ), - ], + // if (customButton.child != null) ...[ + // InkWell( + // onTap: customButton.onTap, + // child: customButton.child, + // ), + // ] else ...[ + // QuillToolbarCustomButton( + // options: + // toolbarConfigurations.buttonOptions.customButtons, + // controller: toolbarConfigurations + // .buttonOptions.customButtons.controller ?? + // globalController, + // ), + // ], + spacerWidget, + ], ]; }, ), ), ); } + + @override + Size get preferredSize => configurations.axis == Axis.horizontal + ? const Size.fromHeight(defaultToolbarSize) + : const Size.fromWidth(defaultToolbarSize); } diff --git a/lib/src/widgets/utils/provider.dart b/lib/src/widgets/utils/provider.dart index 69340b36..12a49553 100644 --- a/lib/src/widgets/utils/provider.dart +++ b/lib/src/widgets/utils/provider.dart @@ -9,6 +9,7 @@ class QuillProvider extends InheritedWidget { const QuillProvider({ required this.configurations, required super.child, + super.key, }); /// Controller object which establishes a link between a rich text document @@ -67,6 +68,7 @@ class QuillToolbarProvider extends InheritedWidget { const QuillToolbarProvider({ required super.child, required this.toolbarConfigurations, + super.key, }); /// The configurations for the toolbar widget of flutter quill @@ -96,7 +98,7 @@ class QuillToolbarProvider extends InheritedWidget { 'because ' 'The provider is $provider. Please make sure to wrap this widget' ' with' - ' QuillProvider widget. ' + ' QuillToolbarProvider widget. ' 'You might using QuillToolbar so make sure to' ' wrap them with the quill provider widget and setup the required ' 'configurations', @@ -124,6 +126,7 @@ class QuillBaseToolbarProvider extends InheritedWidget { const QuillBaseToolbarProvider({ required super.child, required this.toolbarConfigurations, + super.key, }); /// The configurations for the toolbar widget of flutter quill @@ -154,8 +157,8 @@ class QuillBaseToolbarProvider extends InheritedWidget { 'because ' 'The provider is $provider. Please make sure to wrap this widget' ' with' - ' QuillProvider widget. ' - 'You might using QuillToolbar so make sure to' + ' QuillBaseToolbarProvider widget. ' + 'You might using QuillBaseToolbar so make sure to' ' wrap them with the quill provider widget and setup the required ' 'configurations', 'QuillProvider', @@ -181,6 +184,7 @@ class QuillEditorProvider extends InheritedWidget { const QuillEditorProvider({ required super.child, required this.editorConfigurations, + super.key, }); /// The configurations for the quill editor widget of flutter quill @@ -210,7 +214,7 @@ class QuillEditorProvider extends InheritedWidget { 'because ' 'The provider is $provider. Please make sure to wrap this widget' ' with' - ' QuillProvider widget. ' + ' QuillEditorProvider widget. ' 'You might using QuillEditor so make sure to' ' wrap them with the quill provider widget and setup the required ' 'configurations', diff --git a/lib/translations.dart b/lib/translations.dart index d9a743c5..d54eb374 100644 --- a/lib/translations.dart +++ b/lib/translations.dart @@ -1,3 +1,3 @@ library flutter_quill.translations; -export 'src/translations/toolbar.i18n.dart'; +export 'src/l10n/extensions/localizations.dart'; diff --git a/packages/README.md b/packages/README.md new file mode 100644 index 00000000..0f8fce77 --- /dev/null +++ b/packages/README.md @@ -0,0 +1,14 @@ +# Flutter Quill Packages + +This folder contains packages that add more features to the [FlutterQuill](../README.md) +that might be outside of the packages main purpose + +Pub: [quill_html_converter](https://pub.dev/packages/quill_html_converter) + +## Table of contents +- [Flutter Quill Packages](#flutter-quill-packages) + - [Table of contents](#table-of-contents) + - [Packages](#packages) + +## Packages +- [quill_html_converter](./quill_html_converter/) \ No newline at end of file diff --git a/packages/quill_html_converter/.gitignore b/packages/quill_html_converter/.gitignore new file mode 100644 index 00000000..96486fd9 --- /dev/null +++ b/packages/quill_html_converter/.gitignore @@ -0,0 +1,30 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +/pubspec.lock +**/doc/api/ +.dart_tool/ +.packages +build/ diff --git a/packages/quill_html_converter/.metadata b/packages/quill_html_converter/.metadata new file mode 100644 index 00000000..6176c000 --- /dev/null +++ b/packages/quill_html_converter/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "d211f42860350d914a5ad8102f9ec32764dc6d06" + channel: "stable" + +project_type: package diff --git a/packages/quill_html_converter/CHANGELOG.md b/packages/quill_html_converter/CHANGELOG.md new file mode 100644 index 00000000..f3ed5bc7 --- /dev/null +++ b/packages/quill_html_converter/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.0.1-experimental.1 + +* initial release. diff --git a/packages/quill_html_converter/LICENSE b/packages/quill_html_converter/LICENSE new file mode 100644 index 00000000..e82b91ed --- /dev/null +++ b/packages/quill_html_converter/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Flutter Quill Team + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/quill_html_converter/README.md b/packages/quill_html_converter/README.md new file mode 100644 index 00000000..1cada7e2 --- /dev/null +++ b/packages/quill_html_converter/README.md @@ -0,0 +1,41 @@ +# Flutter Quill HTML +A extension for [flutter_quill](https://pub.dev/packages/flutter_quill) package to add support for dealing with conversion to/from html + +It uses [vsc_quill_delta_to_html](https://pub.dev/packages/vsc_quill_delta_to_html) package to convert the the delta to HTML + +This library is **experimental** and the support might be dropped at anytime. + +## Features + +```markdown +- Easy to use +- Support Flutter Quill package +``` + +## Getting started + +```yaml +dependencies: + quill_html_converter: ^ +``` + +## Usage + +First, you need to [setup](../../README.md#usage) the `flutter_quill` first + +Then you can simply convert to/from HTML + +```dart +import 'package:quill_html_converter/quill_html_converter.dart'; + +// Convert Delta to HTML +final html = _controller.document.toDelta().toHtml(); + +// Load Delta document using HTML +_controller.document = + Document.fromDelta(DeltaHtmlExt.fromHtml(html)); +``` + +## Additional information + +This will be updated soon. diff --git a/packages/quill_html_converter/analysis_options.yaml b/packages/quill_html_converter/analysis_options.yaml new file mode 100644 index 00000000..a8fe6f69 --- /dev/null +++ b/packages/quill_html_converter/analysis_options.yaml @@ -0,0 +1,36 @@ +include: package:flutter_lints/flutter.yaml + +analyzer: + errors: + undefined_prefixed_name: ignore + unsafe_html: 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: true + avoid_redundant_argument_values: true + 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 \ No newline at end of file diff --git a/packages/quill_html_converter/lib/quill_html_converter.dart b/packages/quill_html_converter/lib/quill_html_converter.dart new file mode 100644 index 00000000..72645e99 --- /dev/null +++ b/packages/quill_html_converter/lib/quill_html_converter.dart @@ -0,0 +1,60 @@ +library quill_html_converter; + +import 'dart:convert' show jsonDecode; + +import 'package:delta_markdown_converter/delta_markdown_converter.dart' + as delta_markdown show markdownToDelta; +import 'package:flutter_quill/flutter_quill.dart' show Delta; +import 'package:html2md/html2md.dart' as html2md; +import 'package:vsc_quill_delta_to_html/vsc_quill_delta_to_html.dart' + as conventer show ConverterOptions, QuillDeltaToHtmlConverter; + +typedef ConverterOptions = conventer.ConverterOptions; + +/// A extension for [Delta] which comes from `flutter_quill` to extends +/// the functionality of it to support converting the [Delta] to/from HTML +extension DeltaHtmlExt on Delta { + /// Convert the [Delta] instance to HTML Raw string + /// + /// It will run using the following steps: + /// + /// 1. Convert the [Delta] to json using [toJson] + /// 2. Cast the json map as `List>` + /// 3. Pass it to the conventer `vsc_quill_delta_to_html` which is a package + /// that designed specifically for converting the quill delta to html + String toHtml({ConverterOptions? options}) { + final json = toJson(); + final html = conventer.QuillDeltaToHtmlConverter( + List.castFrom(json), + options, + ).convert(); + return html; + } + + /// Convert the HTML Raw string to [Delta] + /// + /// It will run using the following steps: + /// + /// 1. Convert the html to markdown string using `html2md` package + /// 2. Convert the markdown string to quill delta json string + /// 3. Decode the delta json string to [Delta] + /// + /// for more [info](https://github.com/singerdmx/flutter-quill/issues/1100) + static Delta fromHtml(String html) { + final markdown = html2md + .convert( + html, + ) + .replaceAll('unsafe:', ''); + final deltaJsonString = delta_markdown.markdownToDelta(markdown); + final deltaJson = jsonDecode(deltaJsonString); + if (deltaJson is! List) { + throw ArgumentError( + 'The delta json string should be of type list when jsonDecode() it', + ); + } + return Delta.fromJson( + deltaJson, + ); + } +} diff --git a/packages/quill_html_converter/pubspec.yaml b/packages/quill_html_converter/pubspec.yaml new file mode 100644 index 00000000..17333039 --- /dev/null +++ b/packages/quill_html_converter/pubspec.yaml @@ -0,0 +1,34 @@ +name: quill_html_converter +description: A extension for flutter_quill package to add support for dealing with conversion to/from html +version: 0.0.1-experimental.1 +homepage: https://github.com/singerdmx/flutter-quill/tree/master/packages/quill_html_converter +repository: https://github.com/singerdmx/flutter-quill/tree/master/packages/quill_html_converter + +topics: + - ui + - widgets + - widget + - rich-text-editor + - quill + +environment: + sdk: '>=3.1.5 <4.0.0' + flutter: ">=1.17.0" + +dependencies: + flutter: + sdk: flutter + flutter_quill: ^8.5.1 + vsc_quill_delta_to_html: ^1.0.3 + html2md: ^1.3.1 + # markdown: ^7.1.1 + delta_markdown_converter: ^0.0.2 + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^3.0.1 + +flutter: + + uses-material-design: true \ No newline at end of file diff --git a/packages/quill_html_converter/pubspec_overrides.yaml.disabled b/packages/quill_html_converter/pubspec_overrides.yaml.disabled new file mode 100644 index 00000000..844dcdea --- /dev/null +++ b/packages/quill_html_converter/pubspec_overrides.yaml.disabled @@ -0,0 +1,3 @@ +dependency_overrides: + flutter_quill: + path: ../../ \ No newline at end of file diff --git a/packages/quill_html_converter/test/quill_html_converter.dart b/packages/quill_html_converter/test/quill_html_converter.dart new file mode 100644 index 00000000..cd2cad61 --- /dev/null +++ b/packages/quill_html_converter/test/quill_html_converter.dart @@ -0,0 +1,7 @@ +import 'package:flutter_test/flutter_test.dart'; + +void main() { + test('No tests for now', () { + expect(true, true); + }); +} diff --git a/pubspec.yaml b/pubspec.yaml index 8fd42e6e..fce4127e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,13 +1,26 @@ name: flutter_quill description: A rich text editor built for the modern Android, iOS, web and desktop platforms. It is the WYSIWYG editor and a Quill component for Flutter. -version: 8.1.1 +version: 8.5.5 homepage: https://1o24bbs.com/c/bulletjournal/108 repository: https://github.com/singerdmx/flutter-quill + topics: - ui - widgets - widget - rich-text-editor + - quill + +screenshots: + - description: 'Screenshot 1' + path: example/assets/images/screenshot_1.png + - description: 'Screenshot 2' + path: example/assets/images/screenshot_2.png + - description: 'Screenshot 3' + path: example/assets/images/screenshot_3.png + - description: 'Screenshot 4' + path: example/assets/images/screenshot_4.png + platforms: android: ios: @@ -17,28 +30,41 @@ platforms: windows: environment: - sdk: ">=2.17.0 <4.0.0" + sdk: '>=3.1.5 <4.0.0' flutter: ">=3.10.0" dependencies: flutter: sdk: flutter + + flutter_localizations: + sdk: flutter + intl: ^0.18.1 + + # Normal packages collection: ^1.17.0 flutter_colorpicker: ^1.0.3 - flutter_keyboard_visibility: ^5.4.1 quiver: ^3.2.1 - url_launcher: ^6.1.14 - pedantic: ^1.11.1 characters: ^1.3.0 diff_match_patch: ^0.4.1 - i18n_extension: ^9.0.2 - device_info_plus: ^9.1.0 - platform: ^3.1.3 - pasteboard: ^0.2.0 equatable: ^2.0.5 flutter_animate: ^4.2.0+1 + meta: ^1.9.1 + + # Plugins + url_launcher: ^6.1.14 + flutter_keyboard_visibility: ^5.4.1 + device_info_plus: ^9.1.0 + pasteboard: ^0.2.0 +dev_dependencies: + flutter_lints: ^3.0.1 flutter_test: sdk: flutter + flutter_quill_test: ^0.0.4 + test: ^1.24.3 + intl_translation: ^0.18.2 -flutter: null \ No newline at end of file +flutter: + uses-material-design: true + generate: true \ No newline at end of file diff --git a/pubspec_overrides.yaml.disabled b/pubspec_overrides.yaml.disabled new file mode 100644 index 00000000..0c0f849c --- /dev/null +++ b/pubspec_overrides.yaml.disabled @@ -0,0 +1,3 @@ +dependency_overrides: + flutter_quill_test: + path: ./flutter_quill_test \ No newline at end of file diff --git a/before-push.sh b/scripts/before-push.sh similarity index 100% rename from before-push.sh rename to scripts/before-push.sh diff --git a/scripts/disable_local_dev.sh b/scripts/disable_local_dev.sh new file mode 100755 index 00000000..a64ba486 --- /dev/null +++ b/scripts/disable_local_dev.sh @@ -0,0 +1,27 @@ +#!/bin/bash + +# Please make sure to run this script in the root directory of the repository and not inside sub-folders + +echo "" + +echo "Disable local development for flutter_quill..." +rm pubspec_overrides.yaml + +echo "" + +echo "Enable local development for flutter_quill_extensions..." +rm flutter_quill_extensions/pubspec_overrides.yaml + +echo "" + +echo "Enable local development for flutter_quill_test..." +rm flutter_quill_test/pubspec_overrides.yaml + +echo "" + +echo "Disable local development for all the other packages..." +rm packages/quill_html_converter/pubspec_overrides.yaml + +echo "" + +echo "Local development for all libraries has been disabled, please 'flutter pub get' for each one of them" \ No newline at end of file diff --git a/scripts/enable_local_dev.sh b/scripts/enable_local_dev.sh new file mode 100755 index 00000000..603465bc --- /dev/null +++ b/scripts/enable_local_dev.sh @@ -0,0 +1,27 @@ +#!/bin/bash + +# Please make sure to run this script in the root directory of the repository and not inside sub-folders + +echo "" + +echo "Enable local development for flutter_quill..." +cp pubspec_overrides.yaml.disabled pubspec_overrides.yaml + +echo "" + +echo "Enable local development for flutter_quill_extensions..." +cp flutter_quill_extensions/pubspec_overrides.yaml.disabled flutter_quill_extensions/pubspec_overrides.yaml + +echo "" + +echo "Enable local development for flutter_quill_test..." +cp flutter_quill_test/pubspec_overrides.yaml.disabled flutter_quill_test/pubspec_overrides.yaml + +echo "" + +echo "Enable local development for all the other packages..." +cp packages/quill_html_converter/pubspec_overrides.yaml.disabled packages/quill_html_converter/pubspec_overrides.yaml + +echo "" + +echo "Local development for all libraries has been enabled, please 'flutter pub get' for each one of them" \ No newline at end of file diff --git a/scripts/regenerate-translations.sh b/scripts/regenerate-translations.sh new file mode 100755 index 00000000..1f7c26e5 --- /dev/null +++ b/scripts/regenerate-translations.sh @@ -0,0 +1,27 @@ +#!/bin/bash + +# Important: make sure to run the script in the root folder of the repo: +# ./scripts/regenerate-translations.sh +# otherwise the script could delete the wrong folder in rare cases + +echo "" + +echo "Delete the current generated localizations..." +rm -rf lib/src/l10n/generated +echo "" + +echo "Run flutter pub get.." +flutter pub get +echo "" + +echo "Run flutter gen-l10n" +flutter gen-l10n +echo "" + +echo "" +echo "Apply dart fixes to the newly generated files" +dart fix --apply ./lib/src/l10n/generated + +echo "" +echo "Formate the newly generated dart files" +dart format ./lib/src/l10n/generated \ No newline at end of file diff --git a/test/bug_fix_test.dart b/test/bug_fix_test.dart index 6ade28f5..81849459 100644 --- a/test/bug_fix_test.dart +++ b/test/bug_fix_test.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_quill/flutter_quill.dart'; -import 'package:flutter_quill/flutter_quill_test.dart'; +import 'package:flutter_quill_test/flutter_quill_test.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { @@ -12,17 +12,21 @@ void main() { (tester) async { const tooltip = 'custom button'; + final controller = QuillController.basic(); + await tester.pumpWidget( MaterialApp( home: QuillProvider( configurations: QuillConfigurations( - controller: QuillController.basic(), + controller: controller, ), child: const QuillToolbar( configurations: QuillToolbarConfigurations( showRedo: false, customButtons: [ - QuillCustomButton(tooltip: tooltip), + QuillToolbarCustomButtonOptions( + tooltip: tooltip, + ) ], ), ), @@ -130,7 +134,7 @@ void main() { controller.formatSelection(Attribute.unchecked); editor.focusNode.unfocus(); await tester.pump(); - await tester.tap(find.byType(CheckboxPoint)); + await tester.tap(find.byType(QuillEditorCheckboxPoint)); expect(tester.takeException(), isNull); }); }); diff --git a/test/utils/platform_test.dart b/test/utils/platform_test.dart new file mode 100644 index 00000000..db4ee75a --- /dev/null +++ b/test/utils/platform_test.dart @@ -0,0 +1,156 @@ +import 'package:flutter/foundation.dart' show TargetPlatform; +import 'package:flutter_quill/src/utils/platform.dart'; +import 'package:test/test.dart'; + +void main() { + group('Test platform checking logic', () { + var platform = TargetPlatform.linux; + test('Check isDesktop()', () { + platform = TargetPlatform.android; + expect( + isDesktop( + platform: platform, + supportWeb: true, + ), + false, + ); + + for (final desktopPlatform in [ + TargetPlatform.macOS, + TargetPlatform.linux, + TargetPlatform.windows + ]) { + expect( + isDesktop( + supportWeb: false, + overrideIsWeb: false, + platform: desktopPlatform, + ), + true, + ); + + expect( + isDesktop( + supportWeb: false, + overrideIsWeb: true, + platform: desktopPlatform, + ), + false, + ); + + expect( + isDesktop( + supportWeb: true, + overrideIsWeb: true, + platform: desktopPlatform, + ), + true, + ); + } + }); + test('Check isMobile()', () { + platform = TargetPlatform.macOS; + expect( + isMobile( + platform: platform, + supportWeb: true, + ), + false, + ); + + for (final mobilePlatform in [ + TargetPlatform.android, + TargetPlatform.iOS, + ]) { + expect( + isMobile( + platform: mobilePlatform, + supportWeb: false, + overrideIsWeb: false, + ), + true, + ); + + expect( + isMobile( + platform: mobilePlatform, + supportWeb: false, + overrideIsWeb: true, + ), + false, + ); + + expect( + isMobile( + supportWeb: true, + overrideIsWeb: true, + platform: mobilePlatform, + ), + true, + ); + } + }); + test( + 'Check supportWeb parameter when using desktop platform on web', + () { + platform = TargetPlatform.macOS; + expect( + isDesktop( + platform: platform, + supportWeb: true, + ), + true, + ); + expect( + isDesktop( + platform: platform, + supportWeb: false, + overrideIsWeb: false, + ), + true, + ); + + expect( + isDesktop( + platform: platform, + supportWeb: false, + overrideIsWeb: true, + ), + false, + ); + }, + ); + + test( + 'Check supportWeb parameter when using mobile platform on web', + () { + platform = TargetPlatform.android; + expect( + isMobile( + platform: platform, + supportWeb: true, + overrideIsWeb: true, + ), + true, + ); + expect( + isMobile( + platform: platform, + supportWeb: false, + overrideIsWeb: false, + ), + true, + ); + + expect( + isMobile( + platform: platform, + supportWeb: false, + overrideIsWeb: true, + ), + false, + ); + }, + ); + }); +} diff --git a/test/widgets/controller_test.dart b/test/widgets/controller_test.dart index e2405d4b..930cdccf 100644 --- a/test/widgets/controller_test.dart +++ b/test/widgets/controller_test.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_quill/flutter_quill.dart'; -import 'package:flutter_test/flutter_test.dart'; +import 'package:test/test.dart'; void main() { const testDocumentContents = 'data'; @@ -9,7 +9,7 @@ void main() { setUp(() { controller = QuillController.basic() ..compose(Delta()..insert(testDocumentContents), - const TextSelection.collapsed(offset: 0), ChangeSource.LOCAL); + const TextSelection.collapsed(offset: 0), ChangeSource.local); }); group('controller', () { @@ -31,7 +31,7 @@ void main() { controller ..formatText(0, 5, Attribute.h1) ..updateSelection(const TextSelection(baseOffset: 0, extentOffset: 4), - ChangeSource.LOCAL); + ChangeSource.local); expect(controller.getSelectionStyle().values, [Attribute.h1]); }); @@ -41,7 +41,7 @@ void main() { // With selection range controller ..updateSelection(const TextSelection(baseOffset: 0, extentOffset: 4), - ChangeSource.LOCAL) + ChangeSource.local) ..addListener(() { listenerCalled = true; }) @@ -58,12 +58,12 @@ void main() { // With collapsed selection controller ..updateSelection( - const TextSelection.collapsed(offset: 0), ChangeSource.LOCAL) + const TextSelection.collapsed(offset: 0), ChangeSource.local) ..indentSelection(true); expect(controller.getSelectionStyle().values, [Attribute.indentL1]); controller ..updateSelection( - const TextSelection.collapsed(offset: 0), ChangeSource.LOCAL) + const TextSelection.collapsed(offset: 0), ChangeSource.local) ..indentSelection(true); expect(controller.getSelectionStyle().values, [Attribute.indentL2]); controller.indentSelection(false); @@ -75,17 +75,17 @@ void main() { test('indentSelection with multiline document', () { controller ..compose(Delta()..insert('line1\nline2\nline3\n'), - const TextSelection.collapsed(offset: 0), ChangeSource.LOCAL) + const TextSelection.collapsed(offset: 0), ChangeSource.local) // Indent first line ..updateSelection( - const TextSelection.collapsed(offset: 0), ChangeSource.LOCAL) + const TextSelection.collapsed(offset: 0), ChangeSource.local) ..indentSelection(true); expect(controller.getSelectionStyle().values, [Attribute.indentL1]); // Indent first two lines controller ..updateSelection(const TextSelection(baseOffset: 0, extentOffset: 11), - ChangeSource.LOCAL) + ChangeSource.local) ..indentSelection(true); // Should have both L1 and L2 indent attributes in selection. @@ -101,7 +101,7 @@ void main() { TextSelection( baseOffset: 12, extentOffset: controller.document.toPlainText().length - 1), - ChangeSource.LOCAL); + ChangeSource.local); expect(controller.getAllSelectionStyles(), everyElement(const Style())); }); @@ -110,7 +110,7 @@ void main() { ..formatText(0, 2, Attribute.bold) ..replaceText(2, 2, BlockEmbed.image('/test'), null) ..updateSelection(const TextSelection(baseOffset: 0, extentOffset: 4), - ChangeSource.REMOTE); + ChangeSource.remote); final result = controller.getAllIndividualSelectionStylesAndEmbed(); expect(result.length, 2); expect(result[0].offset, 0); @@ -121,7 +121,7 @@ void main() { test('getPlainText', () { controller.updateSelection( const TextSelection(baseOffset: 0, extentOffset: 4), - ChangeSource.LOCAL); + ChangeSource.local); expect(controller.getPlainText(), testDocumentContents); }); @@ -135,7 +135,7 @@ void main() { test('undo', () { var listenerCalled = false; controller.updateSelection( - const TextSelection.collapsed(offset: 4), ChangeSource.LOCAL); + const TextSelection.collapsed(offset: 4), ChangeSource.local); expect( controller.document.toDelta(), @@ -153,7 +153,7 @@ void main() { test('redo', () { var listenerCalled = false; controller.updateSelection( - const TextSelection.collapsed(offset: 4), ChangeSource.LOCAL); + const TextSelection.collapsed(offset: 4), ChangeSource.local); expect(controller.document.toDelta(), Delta()..insert('data\n')); controller.undo(); @@ -222,7 +222,7 @@ void main() { var listenerCalled = false; controller ..updateSelection(const TextSelection(baseOffset: 0, extentOffset: 2), - ChangeSource.LOCAL) + ChangeSource.local) ..addListener(() { listenerCalled = true; }) @@ -238,7 +238,7 @@ void main() { var listenerCalled = false; controller ..updateSelection( - const TextSelection.collapsed(offset: 4), ChangeSource.LOCAL) + const TextSelection.collapsed(offset: 4), ChangeSource.local) ..addListener(() { listenerCalled = true; }); @@ -281,7 +281,7 @@ void main() { ..addListener(() { listenerCalled = true; }) - ..updateSelection(selection, ChangeSource.LOCAL); + ..updateSelection(selection, ChangeSource.local); expect(listenerCalled, isTrue); expect(controller.selection, selection); @@ -295,7 +295,7 @@ void main() { listenerCalled = true; }) ..compose(Delta()..insert('test '), - const TextSelection.collapsed(offset: 0), ChangeSource.LOCAL); + const TextSelection.collapsed(offset: 0), ChangeSource.local); expect(listenerCalled, isTrue); expect(controller.document.toDelta(), diff --git a/test/widgets/editor_test.dart b/test/widgets/editor_test.dart index adaaea49..f063e429 100644 --- a/test/widgets/editor_test.dart +++ b/test/widgets/editor_test.dart @@ -3,8 +3,7 @@ import 'dart:convert' show jsonDecode; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_quill/flutter_quill.dart'; -import 'package:flutter_quill/flutter_quill_test.dart'; -import 'package:flutter_quill/src/widgets/raw_editor/raw_editor.dart'; +import 'package:flutter_quill_test/flutter_quill_test.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { @@ -95,7 +94,7 @@ void main() { expect(latestUri, equals(uri)); }); - Widget customBuilder(BuildContext context, RawEditorState state) { + Widget customBuilder(BuildContext context, QuillRawEditorState state) { return AdaptiveTextSelectionToolbar( anchors: state.contextMenuAnchors, children: [