Compare commits

..
7 Commits
Author SHA1 Message Date
Richard Shiue ee404369ff fix: row document images (#7322)
* fix: row document images

* fix: calendar

* chore: fallback to documentbloc
2025-02-05 16:42:18 +08:00
Lucas.Xu 58ea654ddf chore: update changelog.md 2025-02-05 16:40:49 +08:00
Lucas.Xu cc14793535 chore: bump version 0.8.3 2025-02-05 14:20:18 +08:00
Nathan.fooo 122fb89c87 chore: fix ai page user profile refresh (#7317) 2025-02-04 20:40:06 +08:00
Lucas e9201cce72 fix: editor stuck on image loading loop when uploading image in row document (#7313)
* fix: editor stuck on image loading loop when uploading image in row document

* test: editor stuck on image loading loop when uploading image in row document
2025-02-04 18:03:28 +08:00
Nathan.fooo 5e5693e249 chore: Support new error code (#7311)
* chore: fetch model list

* chore: suppor new error code
2025-02-03 20:52:31 +08:00
Nathan 5ac0eded3c chore: fetch model list 2025-02-01 23:50:58 +08:00
495 changed files with 6244 additions and 15226 deletions

No files matched your search

+1 -1
View File
@@ -18,7 +18,7 @@ on:
env:
CARGO_TERM_COLOR: always
FLUTTER_VERSION: "3.27.4"
FLUTTER_VERSION: "3.22.3"
RUST_TOOLCHAIN: "1.81.0"
CARGO_MAKE_VERSION: "0.37.18"
CLOUD_VERSION: 0.6.54-amd64
+11 -11
View File
@@ -25,7 +25,7 @@ on:
env:
CARGO_TERM_COLOR: always
FLUTTER_VERSION: "3.27.4"
FLUTTER_VERSION: "3.22.2"
RUST_TOOLCHAIN: "1.81.0"
CARGO_MAKE_VERSION: "0.37.18"
CLOUD_VERSION: 0.6.54-amd64
@@ -40,7 +40,7 @@ jobs:
strategy:
fail-fast: true
matrix:
os: [ubuntu-latest]
os: [ ubuntu-latest ]
include:
- os: ubuntu-latest
flutter_profile: development-linux-x86_64
@@ -74,7 +74,7 @@ jobs:
strategy:
fail-fast: true
matrix:
os: [windows-latest]
os: [ windows-latest ]
include:
- os: windows-latest
flutter_profile: development-windows-x86
@@ -101,7 +101,7 @@ jobs:
strategy:
fail-fast: true
matrix:
os: [macos-latest]
os: [ macos-latest ]
include:
- os: macos-latest
flutter_profile: development-mac-x86_64
@@ -123,12 +123,12 @@ jobs:
flutter_profile: ${{ matrix.flutter_profile }}
unit_test:
needs: [prepare-linux]
needs: [ prepare-linux ]
if: github.event.pull_request.draft != true
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest]
os: [ ubuntu-latest ]
include:
- os: ubuntu-latest
flutter_profile: development-linux-x86_64
@@ -217,11 +217,11 @@ jobs:
shell: bash
cloud_integration_test:
needs: [prepare-linux]
needs: [ prepare-linux ]
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest]
os: [ ubuntu-latest ]
include:
- os: ubuntu-latest
flutter_profile: development-linux-x86_64
@@ -340,13 +340,13 @@ jobs:
shell: bash
integration_test:
needs: [prepare-linux]
needs: [ prepare-linux ]
if: github.event.pull_request.draft != true
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest]
test_number: [1, 2, 3, 4, 5, 6, 7, 8, 9]
os: [ ubuntu-latest ]
test_number: [ 1, 2, 3, 4, 5, 6, 7, 8, 9 ]
include:
- os: ubuntu-latest
target: "x86_64-unknown-linux-gnu"
+1 -1
View File
@@ -18,7 +18,7 @@ on:
- "!frontend/appflowy_web_app/**"
env:
FLUTTER_VERSION: "3.27.4"
FLUTTER_VERSION: "3.22.3"
RUST_TOOLCHAIN: "1.81.0"
concurrency:
+11 -11
View File
@@ -6,7 +6,7 @@ on:
- "*"
env:
FLUTTER_VERSION: "3.27.4"
FLUTTER_VERSION: "3.22.0"
RUST_TOOLCHAIN: "1.81.0"
jobs:
@@ -232,10 +232,10 @@ jobs:
matrix:
job:
- {
targets: "aarch64-apple-darwin,x86_64-apple-darwin",
os: macos-latest,
extra-build-args: "",
}
targets: "aarch64-apple-darwin,x86_64-apple-darwin",
os: macos-latest,
extra-build-args: "",
}
steps:
- name: Checkout source code
uses: actions/checkout@v4
@@ -336,12 +336,12 @@ jobs:
matrix:
job:
- {
arch: x86_64,
target: x86_64-unknown-linux-gnu,
os: ubuntu-20.04,
extra-build-args: "",
flutter_profile: production-linux-x86_64,
}
arch: x86_64,
target: x86_64-unknown-linux-gnu,
os: ubuntu-20.04,
extra-build-args: "",
flutter_profile: production-linux-x86_64,
}
steps:
- name: Checkout source code
uses: actions/checkout@v4
+1 -1
View File
@@ -10,7 +10,7 @@ on:
env:
CARGO_TERM_COLOR: always
FLUTTER_VERSION: "3.27.4"
FLUTTER_VERSION: "3.22.0"
RUST_TOOLCHAIN: "1.81.0"
jobs:
+4 -31
View File
@@ -1,37 +1,10 @@
# Release Notes
## Version 0.8.6 - 06/03/2025
### Bug Fixes
- Fix the incorrect title positioning when adjusting the document width setting
- Enhance the user experience of the icon color picker for smoother interactions
- Add missing icons to the database to ensure completeness and consistency
- Resolve the issue with links not functioning correctly on Linux systems
- Improve the outline feature to work seamlessly within columns
- Center the bulleted list icon within columns for better visual alignment
- Enable dragging blocks under tables in the second column to enhance flexibility
- Disable the AI writer feature within tables to prevent conflicts and improve usability
- Automatically enable the header row when converting content from Markdown to ensure proper formatting
- Use the "Undo" function to revert the auto-formatting
## Version 0.8.5 - 04/03/2025
## Version 0.8.3 - 05/02/2025
### New Features
- Columns in Documents: Arrange content side by side using drag-and-drop or the slash menu
- AI Writers: New AI assistants in documents with response formatting options (list, table, text with images, image-only), follow-up questions, contextual memory, and more
- Compact Mode for Databases: Enable compact mode for grid and kanban views (full-page and inline) to increase information density, displaying more data per screen
- Support OpenAI o3-mini model
### Bug Fixes
- Fixed an issue where callout blocks couldn’t be deleted when appearing as the first line in a document
- Fixed a bug preventing the relation field in databases from opening
- Fixed an issue where links in documents were unclickable on Linux
## Version 0.8.4 - 18/02/2025
### New Features
- Switch AI mode on mobile
- Support locking page
- Support uploading svg file as icon
- Support the slash, at, and plus menus on mobile
### Bug Fixes
- Gallery not rendering in row page
- Save image should not copy the image (mobile)
- Support exporting more content to markdown
- Fixed an issue where users were unable to upload images in row pages
- Fixed an issue where users were unable to upload files on Android devices
## Version 0.8.2 - 23/01/2025
### New Features
+14 -12
View File
@@ -1,6 +1,6 @@
<h1 align="center" style="border-bottom: none">
<b>
<a href="https://www.appflowy.com">AppFlowy</a><br>
<a href="https://www.appflowy.io">AppFlowy.IO</a><br>
</b>
⭐️ The Open Source Alternative To Notion ⭐️ <br>
</h1>
@@ -18,18 +18,18 @@ AppFlowy is the AI workspace where you achieve more without losing control of yo
</p>
<p align="center">
<a href="https://www.appflowy.com"><b>Website</b></a> •
<a href="https://www.appflowy.io"><b>Website</b></a> •
<a href="https://forum.appflowy.io/"><b>Forum</b></a> •
<a href="https://discord.gg/9Q2xaN37tV"><b>Discord</b></a> •
<a href="https://www.reddit.com/r/AppFlowy"><b>Reddit</b></a> •
<a href="https://twitter.com/appflowy"><b>Twitter</b></a>
</p>
<p align="center"><img src="https://appflowy.com/_next/static/media/tasks.796c753e.png" alt="AppFlowy Kanban Board for To-dos" /></p>
<p align="center"><img src="https://appflowy.com/_next/static/media/Grid.9e30484b.png" alt="AppFlowy Databases for Tasks and Projects" /></p>
<p align="center"><img src="https://appflowy.com/_next/static/media/sites.a8d5b2b9.png" alt="AppFlowy Sites for Beautiful documentation" /></p>
<p align="center"><img src="https://appflowy.com/_next/static/media/ai.e1460982.png" alt="AppFlowy AI" /></p>
<p align="center"><img src="https://appflowy.com/_next/static/media/template.9ea13c3b.png" alt="AppFlowy Templates" /></p>
<p align="center"><img src="https://appflowy.io/_next/static/media/tasks.796c753e.png" alt="AppFlowy Kanban Board for To-dos" /></p>
<p align="center"><img src="https://appflowy.io/_next/static/media/Grid.9e30484b.png" alt="AppFlowy Databases for Tasks and Projects" /></p>
<p align="center"><img src="https://appflowy.io/_next/static/media/sites.a8d5b2b9.png" alt="AppFlowy Sites for Beautiful documentation" /></p>
<p align="center"><img src="https://appflowy.io/_next/static/media/ai.e1460982.png" alt="AppFlowy AI" /></p>
<p align="center"><img src="https://appflowy.io/_next/static/media/template.9ea13c3b.png" alt="AppFlowy Templates" /></p>
<br></br>
<p align="center" >
@@ -48,7 +48,7 @@ AppFlowy is the AI workspace where you achieve more without losing control of yo
- [App Store](https://apps.apple.com/app/appflowy/id6457261352): iPhone
- [Play Store](https://play.google.com/store/apps/details?id=io.appflowy.appflowy): Android 10 or above; ARMv7 is
not supported
- [Self-hosting AppFlowy](https://appflowy.com/docs/self-host-appflowy-overview)
- [Self-hosting AppFlowy](https://docs.appflowy.io/docs/guides/appflowy/self-hosting-appflowy)
- [Source](https://docs.appflowy.io/docs/documentation/appflowy/from-source)
## Built With
@@ -78,7 +78,7 @@ report [here](https://github.com/AppFlowy-IO/AppFlowy/issues/new?assignees=&labe
## **Releases**
Please see the [changelog](https://appflowy.com/what-is-new) for more details about a given release.
Please see the [changelog](https://www.appflowy.io/whatsnew) for more details about a given release.
## Contributing
@@ -89,7 +89,9 @@ for details.
If your Pull Request is accepted as it fixes a bug, adds functionality, or makes AppFlowy's codebase significantly
easier to use or understand, **Congratulations!** If your administrative and managerial work behind the scenes sustains
the community, **Congratulations!** You are now an official contributor to AppFlowy.
the community, **Congratulations!** You are now an official contributor to AppFlowy. Get in touch with
us ([link](https://tally.so/r/mKP5z3)) to receive the very special Contributor T-shirt!
Proudly wear your T-shirt and show it to us by tagging [@appflowy](https://twitter.com/appflowy) on Twitter.
## Translations 🌎🗺
@@ -150,8 +152,8 @@ more information.
## Acknowledgments
Special thanks to these amazing projects which help power AppFlowy:
Special thanks to these amazing projects which help power AppFlowy.IO:
- [cargo-make](https://github.com/sagiegurari/cargo-make)
- [contrib.rocks](https://contrib.rocks)
- [flutter_chat_ui](https://pub.dev/packages/flutter_chat_ui)
- [flutter_chat_ui](https://pub.dev/packages/flutter_chat_ui)
+1 -1
View File
@@ -4,7 +4,7 @@ workflows:
instance_type: mac_mini_m2
max_build_duration: 30
environment:
flutter: 3.27.4
flutter: 3.22.3
xcode: latest
cocoapods: default
+1 -1
View File
@@ -26,7 +26,7 @@ CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true
CARGO_MAKE_CRATE_FS_NAME = "dart_ffi"
CARGO_MAKE_CRATE_NAME = "dart-ffi"
LIB_NAME = "dart_ffi"
APPFLOWY_VERSION = "0.8.6"
APPFLOWY_VERSION = "0.8.3"
FLUTTER_DESKTOP_FEATURES = "dart"
PRODUCT_NAME = "AppFlowy"
MACOSX_DEPLOYMENT_TARGET = "11.0"
@@ -43,8 +43,6 @@
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java
-->
<meta-data android:name="flutterEmbedding" android:value="2" />
<meta-data android:name="io.flutter.embedding.android.EnableImpeller"
android:value="false" />
</application>
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M16 8C16 12.4183 12.4183 16 8 16C3.58172 16 0 12.4183 0 8C0 3.58172 3.58172 0 8 0C12.4183 0 16 3.58172 16 8ZM9.25 3.75C9.25 4.44036 8.69036 5 8 5C7.30964 5 6.75 4.44036 6.75 3.75C6.75 3.05964 7.30964 2.5 8 2.5C8.69036 2.5 9.25 3.05964 9.25 3.75ZM12 8H9.41901L11.2047 13H9.081L8 9.97321L6.91901 13H4.79528L6.581 8H4V6H12V8Z" fill="#000000"/>
</svg>

Before

Width:  |  Height:  |  Size: 617 B

@@ -1,12 +0,0 @@
output: dist/
releases:
- name: dev
jobs:
- name: release-dev-linux-deb
package:
platform: linux
target: deb
- name: release-dev-linux-rpm
package:
platform: linux
target: rpm
-26
View File
@@ -1,26 +0,0 @@
-----BEGIN PRIVATE KEY-----
MIIEXAIBADCCBDUGByqGSM44BAEwggQoAoICAQDlkozRmUnVH1MJFqOamAmUYu0Y
ruaTrrt6rCIZ0LFrfNnmHA4LOQEcXwBTTyn5sBmkPq+lb/rjmERKhmvl1rfo6q7t
J8mG4TWqSu0tOJQ6QxexnNW4yhzK/r9MS5MQus4Al+y2hQLaAMOUIOnaWIrC9OHy
7xyw+sVipECVKyQqipS4shGUSqbcN+ocQuTB+I0MtIjBii0DGSEY3pxQrfNWjHFL
7iTVKiTn3YOWPJQvv3FvEDrN+5xU5JZpD97ZhXaJpLUyOQaNvcPaOELPWcOSJwqH
Opf5b5N/VZ8SGbHNdxy9d5sSChBgtuAOihEhSp6SjFQ9eVHOf4NyJwSEMmi0gpdp
qm4ZQRJUnM2zIi0p9twR9FRYXzrxOs6yGCQEY+xFG93ShTLTj3zMrIyFqBsqEwFy
JiJWYWe/zp0V7UlLP+jXO9u9eghNmly7QVqD2P0qs/1V0jZFRuLWpsv4inau/qMZ
5EhGG4xCJZXfN1pkehy6e05/h+vs5anK3Wa/H8AtY6cK4CpzAanELvn3AH7VLbAh
Lswu6d5CV+DoFgxCWMzGBSdmCYU+2wRLaL8Q9TZHDR+pvQlunEFdfFoGES9WjBPh
AsVA6Mq22U8XSje9yHI3X9Eqe/7a+ajSgcGmB7oQ11+4xf5h2PtubRW/JL0KMjxC
xMTpq1md6Ndx/ptBUwIdAIOyiKb2YcTLWAOt+LAlRXMsY1+W4pTXJfV6RcMCggIA
Pxbd0HNj2O/aQhJxNZDMBIcx6+cZ+LKch7qLcaEpVqWHvDSnR2eOJJzWn0RoKK+V
uix/4T8texSQkWxAeFFdo6kyrR9XNL7hqEFFq8o9VpmvRzvG6h/bBgh3AHAQE3p/
8WrbK13IhnlWqd0MjFufSphm63o0gaWl95j+6KeUoKQnioetu9HiMtFKx0d/KYqT
QJg7hvR6VNCU2oShfXR3ce7RnUYwD37+djrUjUkoAZkZq2KoxBiKyeoSIeqAme19
tKcOs6b17mhALELuJ+NtDwlDunyiCDUYX9lTPijHwKeIFtBs38+OtRk3aIqmWTQd
bsCzAxp+kUMA5ESBME/RBNCSPHuDvtA3wfWvNbA5DXfZLwCgNSxhekq8XntIsRzf
J4v4uPzKFcVM3+sUUfSF04HHC9ol+PpLqXUyMnskiizqxFPq7H+6tyFZ7X2HiG6T
jcfVWthmv+JyfcABjVnk2qFH7GagENbdtYmfUox13LhE59Sh5chaJnCFtCDp8NCl
WgZnixCOFQ9EgTLaH6MovTvWpEgG2MfBCu5SMUHi2qSflorqpRFH+rA7NZSnyz3w
m7NB+fJSOP0IjEkOh7MafU6Z61oK9WY/Fc+F1zIENVv8PUc3p75y/4RAp4xzyKcT
ilaNC9U/3MRr3QmWwY7ejtZx6xdOxsvWBRDRSNbDdIkEHgIcfy0+ZHp+4MBcWSDv
uzWeM8QmNvbP+owM+H4F7A==
-----END PRIVATE KEY-----
-36
View File
@@ -1,36 +0,0 @@
-----BEGIN PUBLIC KEY-----
MIIGQzCCBDUGByqGSM44BAEwggQoAoICAQDlkozRmUnVH1MJFqOamAmUYu0YruaT
rrt6rCIZ0LFrfNnmHA4LOQEcXwBTTyn5sBmkPq+lb/rjmERKhmvl1rfo6q7tJ8mG
4TWqSu0tOJQ6QxexnNW4yhzK/r9MS5MQus4Al+y2hQLaAMOUIOnaWIrC9OHy7xyw
+sVipECVKyQqipS4shGUSqbcN+ocQuTB+I0MtIjBii0DGSEY3pxQrfNWjHFL7iTV
KiTn3YOWPJQvv3FvEDrN+5xU5JZpD97ZhXaJpLUyOQaNvcPaOELPWcOSJwqHOpf5
b5N/VZ8SGbHNdxy9d5sSChBgtuAOihEhSp6SjFQ9eVHOf4NyJwSEMmi0gpdpqm4Z
QRJUnM2zIi0p9twR9FRYXzrxOs6yGCQEY+xFG93ShTLTj3zMrIyFqBsqEwFyJiJW
YWe/zp0V7UlLP+jXO9u9eghNmly7QVqD2P0qs/1V0jZFRuLWpsv4inau/qMZ5EhG
G4xCJZXfN1pkehy6e05/h+vs5anK3Wa/H8AtY6cK4CpzAanELvn3AH7VLbAhLswu
6d5CV+DoFgxCWMzGBSdmCYU+2wRLaL8Q9TZHDR+pvQlunEFdfFoGES9WjBPhAsVA
6Mq22U8XSje9yHI3X9Eqe/7a+ajSgcGmB7oQ11+4xf5h2PtubRW/JL0KMjxCxMTp
q1md6Ndx/ptBUwIdAIOyiKb2YcTLWAOt+LAlRXMsY1+W4pTXJfV6RcMCggIAPxbd
0HNj2O/aQhJxNZDMBIcx6+cZ+LKch7qLcaEpVqWHvDSnR2eOJJzWn0RoKK+Vuix/
4T8texSQkWxAeFFdo6kyrR9XNL7hqEFFq8o9VpmvRzvG6h/bBgh3AHAQE3p/8Wrb
K13IhnlWqd0MjFufSphm63o0gaWl95j+6KeUoKQnioetu9HiMtFKx0d/KYqTQJg7
hvR6VNCU2oShfXR3ce7RnUYwD37+djrUjUkoAZkZq2KoxBiKyeoSIeqAme19tKcO
s6b17mhALELuJ+NtDwlDunyiCDUYX9lTPijHwKeIFtBs38+OtRk3aIqmWTQdbsCz
Axp+kUMA5ESBME/RBNCSPHuDvtA3wfWvNbA5DXfZLwCgNSxhekq8XntIsRzfJ4v4
uPzKFcVM3+sUUfSF04HHC9ol+PpLqXUyMnskiizqxFPq7H+6tyFZ7X2HiG6TjcfV
Wthmv+JyfcABjVnk2qFH7GagENbdtYmfUox13LhE59Sh5chaJnCFtCDp8NClWgZn
ixCOFQ9EgTLaH6MovTvWpEgG2MfBCu5SMUHi2qSflorqpRFH+rA7NZSnyz3wm7NB
+fJSOP0IjEkOh7MafU6Z61oK9WY/Fc+F1zIENVv8PUc3p75y/4RAp4xzyKcTilaN
C9U/3MRr3QmWwY7ejtZx6xdOxsvWBRDRSNbDdIkDggIGAAKCAgEAt1DHYZoeXY0r
vYXmxdNO6zfnbz1GGZHXpakzm9h4BrxPDP5J8DQ9ZeVVKg5+cU9AyMO3cZHp7wkx
k6IB+ZDUpqO1D3lWriRl2fI8cS4edI0fzpnW1nyhhFD4MbKmP+v27aH+DhZ4Up3y
GMmJTLmKiYx1EgZp7Sx77PBYDVMsKKd3h9+Hjp2YtUTfD2lleAmC+wcQGZiNtGw/
eKpsmUVnWrepOdntWTtCQi1OvfcHaF2QmgktCq+68hbDNYWaXmzVIiQqrdv/zzOG
hCFIrRGWemrxL0iFG4Pzc4UfOINsISQLcUxRuF6pQWPxF8O/mWKfzAeqWxmIujUM
EoSEuI3yQ8VjlYpW/8FSK7UhgnHBHOpCJWPWs/vQXAnaUR2PYyzuIzhVEhFs8YA8
iBIKnixIC2hu0YbEk3TBr/TRcbd7mDw9Mq7NT88xzdU13+Wh+4zhdX3rtBHYzBtI
7GaONGUNyY4h0duoyLpH6dxevaeKN6/bEdzYESjoE58QA88CpnAZGhJVphAba4cb
w6GTDhK3RlPWh6hRqJwLDILGtnJS3UKeBDRmKMqNuqmHqPjyAAvt9JBO8lzjoLgf
1cDsXHNWBVwA2jsX2CukNJPlY1Fa3MWhdaUXmy6QGMSisr1sptvBt1Phry8T2u+P
Y29SB4jvwqls268rP0cWqy4WXwlVwuc=
-----END PUBLIC KEY-----
@@ -23,24 +23,24 @@ void main() {
final expandFinder = find.byFlowySvg(FlowySvgs.hamburger_s_s);
// Is expanded by default
expect(collapseFinder, findsNothing);
expect(expandFinder, findsOneWidget);
// Collapse hidden groups
await tester.tap(expandFinder);
await tester.pumpAndSettle();
// Is collapsed
expect(collapseFinder, findsOneWidget);
expect(expandFinder, findsNothing);
// Expand hidden groups
// Collapse hidden groups
await tester.tap(collapseFinder);
await tester.pumpAndSettle();
// Is expanded
// Is collapsed
expect(collapseFinder, findsNothing);
expect(expandFinder, findsOneWidget);
// Expand hidden groups
await tester.tap(expandFinder);
await tester.pumpAndSettle();
// Is expanded
expect(collapseFinder, findsOneWidget);
expect(expandFinder, findsNothing);
});
testWidgets('hide first group, and show it again', (tester) async {
@@ -48,9 +48,6 @@ void main() {
await tester.tapAnonymousSignInButton();
await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Board);
final expandFinder = find.byFlowySvg(FlowySvgs.hamburger_s_s);
await tester.tapButton(expandFinder);
// Tap the options of the first group
final optionsFinder = find
.descendant(
@@ -31,6 +31,12 @@ void main() {
await tester.enterUserName('local_user');
// Scroll to sign-in
await tester.scrollUntilVisible(
find.byType(AccountSignInOutButton),
100,
scrollable: find.findSettingsScrollable(),
);
await tester.tapButton(find.byType(AccountSignInOutButton));
// sign up with Google
@@ -33,7 +33,7 @@ void main() {
await tester.editor.tapSlashMenuItemWithName(
LocaleKeys.document_slashMenu_name_aiWriter.tr(),
);
expect(find.byType(AiWriterBlockComponent), findsOneWidget);
expect(find.byType(AIWriterBlockComponent), findsOneWidget);
// switch to another page
await tester.openPage(Constants.gettingStartedPageName);
@@ -41,7 +41,7 @@ void main() {
await tester.openPage(pageName);
// expect the ai writer block is not in the document
expect(find.byType(AiWriterBlockComponent), findsNothing);
expect(find.byType(AIWriterBlockComponent), findsNothing);
});
});
}
@@ -57,7 +57,7 @@ void main() {
// move the checkbox to the child of the block at path [9]
await tester.editor.dragBlock(
[10],
const Offset(120, -20),
const Offset(80, -30),
);
// wait for the move animation to complete
@@ -57,6 +57,12 @@ void main() {
await tester.openSettings();
await tester.openSettingsPage(SettingsPage.account);
// Scroll to sign-in
await tester.scrollUntilVisible(
find.byType(AccountSignInOutButton),
100,
scrollable: find.findSettingsScrollable(),
);
await tester.tapButton(find.byType(AccountSignInOutButton));
tester.expectToSeeGoogleLoginButton();
@@ -75,7 +75,7 @@ void main() {
[
LogicalKeyboardKey.control,
LogicalKeyboardKey.shift,
LogicalKeyboardKey.keyC,
LogicalKeyboardKey.keyE,
],
tester: tester,
withKeyUp: true,
@@ -458,7 +458,7 @@ void main() {
});
testWidgets('paste the image url', (tester) async {
const plainText = 'http://example.com/1.jpg';
const plainText = 'https://appflowy.io/1.jpg';
final image = await rootBundle.load('assets/test/images/sample.jpeg');
final bytes = image.buffer.asUint8List();
await tester.pasteContent(plainText: plainText, image: ('jpeg', bytes),
@@ -4,7 +4,6 @@ import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/style_widget/button.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
@@ -164,55 +163,5 @@ void main() {
lessThan(5),
);
});
testWidgets('insert inline math equation by shortcut', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
// create a new document
await tester.createNewPageWithNameUnderParent(
name: 'insert inline math equation by shortcut',
);
// tap the first line of the document
await tester.editor.tapLineOfEditorAt(0);
// insert a inline page
const formula = 'E = MC ^ 2';
await tester.ime.insertText(formula);
await tester.editor.updateSelection(
Selection.single(path: [0], startOffset: 0, endOffset: formula.length),
);
// mock key event
await tester.simulateKeyEvent(
LogicalKeyboardKey.keyE,
isShiftPressed: true,
isControlPressed: true,
);
// expect to see the math equation block
final inlineMathEquation = find.byType(InlineMathEquation);
expect(inlineMathEquation, findsOneWidget);
await tester.editor.tapLineOfEditorAt(0);
const text = 'Hello World';
await tester.ime.insertText(text);
final inlineText = find.textContaining(text, findRichText: true);
expect(inlineText, findsOneWidget);
// the text should be in the same line with the math equation
final inlineMathEquationPosition = tester.getRect(inlineMathEquation);
final textPosition = tester.getRect(inlineText);
// allow 5px difference
expect(
(textPosition.top - inlineMathEquationPosition.top).abs(),
lessThan(5),
);
expect(
(textPosition.bottom - inlineMathEquationPosition.bottom).abs(),
lessThan(5),
);
});
});
}
@@ -48,7 +48,7 @@ void main() {
await tester.editor.showSlashMenu();
await tester.editor.tapSlashMenuItemWithName(
LocaleKeys.document_slashMenu_name_photoGallery.tr(),
offset: 100,
offset: 80,
);
expect(find.byType(MultiImageBlockComponent), findsOneWidget);
expect(find.byType(MultiImagePlaceholder), findsOneWidget);
@@ -146,7 +146,7 @@ void main() {
await tester.editor.showSlashMenu();
await tester.editor.tapSlashMenuItemWithName(
LocaleKeys.document_slashMenu_name_photoGallery.tr(),
offset: 100,
offset: 80,
);
expect(find.byType(MultiImageBlockComponent), findsOneWidget);
expect(find.byType(MultiImagePlaceholder), findsOneWidget);
@@ -1,3 +1,5 @@
import 'dart:io';
import 'package:appflowy/plugins/base/emoji/emoji_picker.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/emoji_picker_button.dart';
import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';
@@ -5,9 +7,11 @@ import 'package:appflowy/shared/icon_emoji_picker/recent_icons.dart';
import 'package:appflowy/workspace/presentation/widgets/view_title_bar.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:flowy_infra_ui/style_widget/text_field.dart';
import 'package:flowy_svg/flowy_svg.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';
import '../../shared/base.dart';
import '../../shared/common_operations.dart';
@@ -211,12 +215,17 @@ void main() {
}
});
testWidgets('Update page custom image icon in title bar', (tester) async {
testWidgets('Update page custom icon in title bar', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
/// prepare local image
final iconData = await tester.prepareImageIcon();
final imagePath = await rootBundle.load('assets/test/images/sample.jpeg');
final tempDirectory = await getTemporaryDirectory();
final localImagePath = p.join(tempDirectory.path, 'sample.jpeg');
final imageFile = File(localImagePath)
..writeAsBytesSync(imagePath.buffer.asUint8List());
final iconData = EmojiIconData.custom(imageFile.path);
// create document, board, grid and calendar views
for (final value in ViewLayoutPB.values) {
@@ -250,97 +259,4 @@ void main() {
);
}
});
testWidgets('Update page custom svg icon in title bar', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
/// prepare local image
final iconData = await tester.prepareSvgIcon();
// create document, board, grid and calendar views
for (final value in ViewLayoutPB.values) {
if (value == ViewLayoutPB.Chat) {
continue;
}
await tester.createNewPageWithNameUnderParent(
name: value.name,
parentName: gettingStarted,
layout: value,
);
// update its icon
await tester.updatePageIconInTitleBarByName(
name: value.name,
layout: value,
icon: iconData,
);
tester.expectViewHasIcon(
value.name,
value,
iconData,
);
tester.expectViewTitleHasIcon(
value.name,
value,
iconData,
);
}
});
testWidgets('Update page custom svg icon in title bar by pasting a link',
(tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
/// prepare local image
const testIconLink =
'https://beta.appflowy.cloud/api/file_storage/008e6f23-516b-4d8d-b1fe-2b75c51eee26/v1/blob/6bdf8dff%2D0e54%2D4d35%2D9981%2Dcde68bef1141/BGpLnRtb3AGBNgSJsceu70j83zevYKrMLzqsTIJcBeI=.svg';
/// create document, board, grid and calendar views
for (final value in ViewLayoutPB.values) {
if (value == ViewLayoutPB.Chat) {
continue;
}
await tester.createNewPageWithNameUnderParent(
name: value.name,
parentName: gettingStarted,
layout: value,
);
/// update its icon
await tester.updatePageIconInTitleBarByPasteALink(
name: value.name,
layout: value,
iconLink: testIconLink,
);
/// check if there is a svg in page
final pageName = tester.findPageName(
value.name,
layout: value,
);
final imageInPage = find.descendant(
of: pageName,
matching: find.byType(SvgPicture),
);
expect(imageInPage, findsOneWidget);
/// check if there is a svg in title
final imageInTitle = find.descendant(
of: find.byType(ViewTitleBar),
matching: find.byWidgetPredicate((w) {
if (w is! SvgPicture) return false;
final loader = w.bytesLoader;
if (loader is! SvgFileLoader) return false;
return loader.file.path.endsWith('.svg');
}),
);
expect(imageInTitle, findsOneWidget);
}
});
}
@@ -1,16 +1,12 @@
import 'dart:convert';
import 'dart:io';
import 'package:appflowy/plugins/shared/share/share_button.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pbenum.dart';
import 'package:archive/archive.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:path/path.dart' as p;
import '../../shared/mock/mock_file_picker.dart';
import '../../shared/util.dart';
import '../document/document_with_database_test.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
@@ -22,7 +18,7 @@ void main() {
// mock the file picker
final path = await mockSaveFilePath(
p.join(context.applicationDataDirectory, 'test.zip'),
p.join(context.applicationDataDirectory, 'test.md'),
);
// click the share button and select markdown
await tester.tapShareButton();
@@ -32,14 +28,10 @@ void main() {
tester.expectToExportSuccess();
final file = File(path);
expect(file.existsSync(), true);
final archive = ZipDecoder().decodeBytes(file.readAsBytesSync());
for (final entry in archive) {
if (entry.isFile && entry.name.endsWith('.md')) {
final markdown = utf8.decode(entry.content);
expect(markdown, expectedMarkdown);
}
}
final isExist = file.existsSync();
expect(isExist, true);
final markdown = file.readAsStringSync();
expect(markdown, expectedMarkdown);
});
testWidgets(
@@ -65,7 +57,7 @@ void main() {
final path = await mockSaveFilePath(
p.join(
context.applicationDataDirectory,
'${shareButtonState.view.name}.zip',
'${shareButtonState.view.name}.md',
),
);
@@ -77,44 +69,10 @@ void main() {
tester.expectToExportSuccess();
final file = File(path);
expect(file.existsSync(), true);
final archive = ZipDecoder().decodeBytes(file.readAsBytesSync());
for (final entry in archive) {
if (entry.isFile && entry.name.endsWith('.md')) {
final markdown = utf8.decode(entry.content);
expect(markdown, expectedMarkdown);
}
}
final isExist = file.existsSync();
expect(isExist, true);
},
);
testWidgets('share the markdown with database', (tester) async {
final context = await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await insertLinkedDatabase(tester, ViewLayoutPB.Grid);
// mock the file picker
final path = await mockSaveFilePath(
p.join(context.applicationDataDirectory, 'test.zip'),
);
// click the share button and select markdown
await tester.tapShareButton();
await tester.tapMarkdownButton();
// expect to see the success dialog
tester.expectToExportSuccess();
final file = File(path);
expect(file.existsSync(), true);
final archive = ZipDecoder().decodeBytes(file.readAsBytesSync());
bool hasCsvFile = false;
for (final entry in archive) {
if (entry.isFile && entry.name.endsWith('.csv')) {
hasCsvFile = true;
}
}
expect(hasCsvFile, true);
});
});
}
@@ -1,56 +0,0 @@
import 'package:appflowy/mobile/presentation/inline_actions/mobile_inline_actions_menu.dart';
import 'package:appflowy/mobile/presentation/inline_actions/mobile_inline_actions_menu_group.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import '../../shared/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
const title = 'Test At Menu';
group('at menu', () {
testWidgets('show at menu', (tester) async {
await tester.launchInAnonymousMode();
await tester.createPageAndShowAtMenu(title);
final menuWidget = find.byType(MobileInlineActionsMenu);
expect(menuWidget, findsOneWidget);
});
testWidgets('search by at menu', (tester) async {
await tester.launchInAnonymousMode();
await tester.createPageAndShowAtMenu(title);
const searchText = gettingStarted;
await tester.ime.insertText(searchText);
final actionWidgets = find.byType(MobileInlineActionsWidget);
expect(actionWidgets, findsNWidgets(2));
});
testWidgets('tap at menu', (tester) async {
await tester.launchInAnonymousMode();
await tester.createPageAndShowAtMenu(title);
const searchText = gettingStarted;
await tester.ime.insertText(searchText);
final actionWidgets = find.byType(MobileInlineActionsWidget);
await tester.tap(actionWidgets.last);
expect(find.byType(MentionPageBlock), findsOneWidget);
});
testWidgets('create subpage with at menu', (tester) async {
await tester.launchInAnonymousMode();
await tester.createNewDocumentOnMobile(title);
await tester.editor.tapLineOfEditorAt(0);
const subpageName = 'Subpage';
await tester.ime.insertText('[[$subpageName');
await tester.pumpAndSettle();
final actionWidgets = find.byType(MobileInlineActionsWidget);
await tester.tapButton(actionWidgets.first);
final firstNode =
tester.editor.getCurrentEditorState().getNodeAtPath([0]);
assert(firstNode != null);
expect(firstNode!.delta?.toPlainText().contains('['), false);
});
});
}
@@ -1,11 +1,8 @@
import 'package:integration_test/integration_test.dart';
import 'at_menu_test.dart' as at_menu;
import 'at_menu_test.dart' as at_menu_test;
import 'page_style_test.dart' as page_style_test;
import 'plus_menu_test.dart' as plus_menu_test;
import 'simple_table_test.dart' as simple_table_test;
import 'slash_menu_test.dart' as slash_menu;
import 'title_test.dart' as title_test;
import 'toolbar_test.dart' as toolbar_test;
@@ -16,9 +13,6 @@ void main() {
title_test.main();
page_style_test.main();
plus_menu_test.main();
at_menu_test.main();
simple_table_test.main();
toolbar_test.main();
slash_menu.main();
at_menu.main();
}
@@ -1,12 +1,15 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'dart:io';
import 'package:appflowy/mobile/presentation/base/view_page/app_bar_buttons.dart';
import 'package:appflowy/mobile/presentation/presentation.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/page_style/_page_style_icon.dart';
import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';
import '../../shared/emoji.dart';
import '../../shared/util.dart';
@@ -15,11 +18,16 @@ void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('document title:', () {
testWidgets('update page custom image icon in title bar', (tester) async {
testWidgets('update page custom icon in title bar', (tester) async {
await tester.launchInAnonymousMode();
/// prepare local image
final iconData = await tester.prepareImageIcon();
final imagePath = await rootBundle.load('assets/test/images/sample.jpeg');
final tempDirectory = await getTemporaryDirectory();
final localImagePath = p.join(tempDirectory.path, 'sample.jpeg');
final imageFile = File(localImagePath)
..writeAsBytesSync(imagePath.buffer.asUint8List());
final iconData = EmojiIconData.custom(imageFile.path);
/// create an empty page
await tester
@@ -42,63 +50,16 @@ void main() {
/// check result
final documentPage = find.byType(MobileDocumentScreen);
final rawEmojiIconFinder = find
final rawEmojiIconWidget = find
.descendant(
of: documentPage,
matching: find.byType(RawEmojiIconWidget),
)
.last;
final rawEmojiIconWidget =
rawEmojiIconFinder.evaluate().first.widget as RawEmojiIconWidget;
.evaluate()
.first
.widget as RawEmojiIconWidget;
final iconDataInWidget = rawEmojiIconWidget.emoji;
expect(iconDataInWidget.type, FlowyIconType.custom);
final imageFinder =
find.descendant(of: rawEmojiIconFinder, matching: find.byType(Image));
expect(imageFinder, findsOneWidget);
});
testWidgets('update page custom svg icon in title bar', (tester) async {
await tester.launchInAnonymousMode();
/// prepare local image
final iconData = await tester.prepareSvgIcon();
/// create an empty page
await tester
.tapButton(find.byKey(BottomNavigationBarItemType.add.valueKey));
/// show Page style page
await tester.tapButton(find.byType(MobileViewPageLayoutButton));
final pageStyleIcon = find.byType(PageStyleIcon);
final iconInPageStyleIcon = find.descendant(
of: pageStyleIcon,
matching: find.byType(RawEmojiIconWidget),
);
expect(iconInPageStyleIcon, findsNothing);
/// show icon picker
await tester.tapButton(pageStyleIcon);
/// upload custom icon
await tester.pickImage(iconData);
/// check result
final documentPage = find.byType(MobileDocumentScreen);
final rawEmojiIconFinder = find
.descendant(
of: documentPage,
matching: find.byType(RawEmojiIconWidget),
)
.last;
final rawEmojiIconWidget =
rawEmojiIconFinder.evaluate().first.widget as RawEmojiIconWidget;
final iconDataInWidget = rawEmojiIconWidget.emoji;
expect(iconDataInWidget.type, FlowyIconType.custom);
final svgFinder = find.descendant(
of: rawEmojiIconFinder,
matching: find.byType(SvgPicture),
);
expect(svgFinder, findsOneWidget);
});
});
}
@@ -1,9 +1,6 @@
import 'dart:async';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/inline_actions/mobile_inline_actions_menu.dart';
import 'package:appflowy/mobile/presentation/inline_actions/mobile_inline_actions_menu_group.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart';
@@ -88,32 +85,5 @@ void main() {
equals(Selection.collapsed(Position(path: [2]))),
);
});
const title = 'Test Plus Menu';
testWidgets('show plus menu', (tester) async {
await tester.launchInAnonymousMode();
await tester.createPageAndShowPlusMenu(title);
final menuWidget = find.byType(MobileInlineActionsMenu);
expect(menuWidget, findsOneWidget);
});
testWidgets('search by plus menu', (tester) async {
await tester.launchInAnonymousMode();
await tester.createPageAndShowPlusMenu(title);
const searchText = gettingStarted;
await tester.ime.insertText(searchText);
final actionWidgets = find.byType(MobileInlineActionsWidget);
expect(actionWidgets, findsNWidgets(2));
});
testWidgets('tap plus menu', (tester) async {
await tester.launchInAnonymousMode();
await tester.createPageAndShowPlusMenu(title);
const searchText = gettingStarted;
await tester.ime.insertText(searchText);
final actionWidgets = find.byType(MobileInlineActionsWidget);
await tester.tap(actionWidgets.last);
expect(find.byType(MentionPageBlock), findsOneWidget);
});
});
}
@@ -1,84 +0,0 @@
import 'package:appflowy/mobile/presentation/selection_menu/mobile_selection_menu_item.dart';
import 'package:appflowy/mobile/presentation/selection_menu/mobile_selection_menu_item_widget.dart';
import 'package:appflowy/mobile/presentation/selection_menu/mobile_selection_menu_widget.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/mobile_items.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import '../../shared/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
const title = 'Test Slash Menu';
group('slash menu', () {
testWidgets('show slash menu', (tester) async {
await tester.launchInAnonymousMode();
await tester.createPageAndShowSlashMenu(title);
final menuWidget = find.byType(MobileSelectionMenuWidget);
expect(menuWidget, findsOneWidget);
final items =
(menuWidget.evaluate().first.widget as MobileSelectionMenuWidget)
.items;
int i = 0;
for (final item in items) {
final localItem = mobileItems[i];
expect(item.name, localItem.name);
i++;
}
});
testWidgets('search by slash menu', (tester) async {
await tester.launchInAnonymousMode();
await tester.createPageAndShowSlashMenu(title);
const searchText = 'Heading';
await tester.ime.insertText(searchText);
final itemWidgets = find.byType(MobileSelectionMenuItemWidget);
int number = 0;
for (final item in mobileItems) {
if (item is MobileSelectionMenuItem) {
for (final childItem in item.children) {
if (childItem.name
.toLowerCase()
.contains(searchText.toLowerCase())) {
number++;
}
}
} else {
if (item.name.toLowerCase().contains(searchText.toLowerCase())) {
number++;
}
}
}
expect(itemWidgets, findsNWidgets(number));
});
testWidgets('tap to show submenu', (tester) async {
await tester.launchInAnonymousMode();
await tester.createNewDocumentOnMobile(title);
await tester.editor.tapLineOfEditorAt(0);
final listview = find.descendant(
of: find.byType(MobileSelectionMenuWidget),
matching: find.byType(ListView),
);
for (final item in mobileItems) {
if (item is! MobileSelectionMenuItem) continue;
await tester.editor.showSlashMenu();
await tester.scrollUntilVisible(
find.text(item.name),
50,
scrollable: listview,
duration: const Duration(milliseconds: 250),
);
await tester.tap(find.text(item.name));
final childrenLength = ((listview.evaluate().first.widget as ListView)
.childrenDelegate as SliverChildListDelegate)
.children
.length;
expect(childrenLength, item.children.length);
}
});
});
}
@@ -50,8 +50,6 @@ import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';
import 'package:universal_platform/universal_platform.dart';
import 'emoji.dart';
@@ -617,7 +615,7 @@ extension CommonOperations on WidgetTester {
);
final distanceY = getCenter(to).dy - getCenter(from).dx;
await drag(from, Offset(0, distanceY));
await pumpAndSettle(const Duration(seconds: 1));
await pumpAndSettle();
}
// tap the button with [FlowySvgData]
@@ -679,25 +677,6 @@ extension CommonOperations on WidgetTester {
await pumpAndSettle();
}
Future<void> updatePageIconInTitleBarByPasteALink({
required String name,
required ViewLayoutPB layout,
required String iconLink,
}) async {
await openPage(
name,
layout: layout,
);
final title = find.descendant(
of: find.byType(ViewTitleBar),
matching: find.text(name),
);
await tapButton(title);
await tapButton(find.byType(EmojiPickerButton));
await pasteImageLinkAsIcon(iconLink);
await pumpAndSettle();
}
Future<void> openNotificationHub({int tabIndex = 0}) async {
final finder = find.descendant(
of: find.byType(NotificationButton),
@@ -956,45 +935,6 @@ extension CommonOperations on WidgetTester {
),
);
}
Future<EmojiIconData> prepareImageIcon() async {
final imagePath = await rootBundle.load('assets/test/images/sample.jpeg');
final tempDirectory = await getTemporaryDirectory();
final localImagePath = p.join(tempDirectory.path, 'sample.jpeg');
final imageFile = File(localImagePath)
..writeAsBytesSync(imagePath.buffer.asUint8List());
return EmojiIconData.custom(imageFile.path);
}
Future<EmojiIconData> prepareSvgIcon() async {
final imagePath = await rootBundle.load('assets/test/images/sample.svg');
final tempDirectory = await getTemporaryDirectory();
final localImagePath = p.join(tempDirectory.path, 'sample.svg');
final imageFile = File(localImagePath)
..writeAsBytesSync(imagePath.buffer.asUint8List());
return EmojiIconData.custom(imageFile.path);
}
/// create new page and show slash menu
Future<void> createPageAndShowSlashMenu(String title) async {
await createNewDocumentOnMobile(title);
await editor.tapLineOfEditorAt(0);
await editor.showSlashMenu();
}
/// create new page and show at menu
Future<void> createPageAndShowAtMenu(String title) async {
await createNewDocumentOnMobile(title);
await editor.tapLineOfEditorAt(0);
await editor.showAtMenu();
}
/// create new page and show plus menu
Future<void> createPageAndShowPlusMenu(String title) async {
await createNewDocumentOnMobile(title);
await editor.tapLineOfEditorAt(0);
await editor.showPlusMenu();
}
}
extension SettingsFinder on CommonFinders {
@@ -1464,7 +1464,6 @@ extension AppFlowyDatabaseTest on WidgetTester {
);
await tapButton(button);
await tapButtonWithName(LocaleKeys.button_delete.tr());
}
Future<void> dragDropRescheduleCalendarEvent() async {
@@ -1,24 +1,20 @@
import 'dart:convert';
import 'dart:io';
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';
import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';
import 'package:appflowy/shared/icon_emoji_picker/icon_color_picker.dart';
import 'package:appflowy/shared/icon_emoji_picker/icon_picker.dart';
import 'package:appflowy/shared/icon_emoji_picker/icon_uploader.dart';
import 'package:appflowy/shared/icon_emoji_picker/tab.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/space_icon_popup.dart';
import 'package:cross_file/cross_file.dart';
import 'package:desktop_drop/desktop_drop.dart';
import 'package:flowy_infra_ui/style_widget/primary_rounded_button.dart';
import 'package:flowy_svg/flowy_svg.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_emoji_mart/flutter_emoji_mart.dart';
import 'package:flutter_test/flutter_test.dart';
import 'base.dart';
import 'common_operations.dart';
extension EmojiTestExtension on WidgetTester {
Future<void> tapEmoji(String emoji) async {
@@ -94,7 +90,7 @@ extension EmojiTestExtension on WidgetTester {
final dropTargetWidget = dropTarget.evaluate().first.widget as DropTarget;
dropTargetWidget.onDragDone?.call(
DropDoneDetails(
files: [DropItemFile(icon.emoji)],
files: [XFile(icon.emoji)],
localPosition: Offset.zero,
globalPosition: Offset.zero,
),
@@ -108,37 +104,4 @@ extension EmojiTestExtension on WidgetTester {
);
await tapButton(confirmButton);
}
Future<void> pasteImageLinkAsIcon(String link) async {
final pickTab = find.byType(PickerTab);
expect(pickTab, findsOneWidget);
await pumpAndSettle();
/// switch to custom tab
final iconTab = find.descendant(
of: pickTab,
matching: find.text(PickerTabType.custom.tr),
);
expect(iconTab, findsOneWidget);
await tapButton(iconTab);
// mock the clipboard
await getIt<ClipboardService>()
.setData(ClipboardServiceData(plainText: link));
// paste the link
await simulateKeyEvent(
LogicalKeyboardKey.keyV,
isControlPressed: Platform.isLinux || Platform.isWindows,
isMetaPressed: Platform.isMacOS,
);
await pumpAndSettle(const Duration(seconds: 5));
/// confirm to upload
final confirmButton = find.descendant(
of: find.byType(IconUploader),
matching: find.byType(PrimaryRoundedButton),
);
await tapButton(confirmButton);
}
}
@@ -8,7 +8,6 @@ import 'package:appflowy/plugins/document/presentation/banner.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/document_cover_widget.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart';
import 'package:appflowy/shared/appflowy_network_image.dart';
import 'package:appflowy/shared/appflowy_network_svg.dart';
import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';
import 'package:appflowy/shared/icon_emoji_picker/icon_picker.dart';
import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart';
@@ -253,19 +252,16 @@ extension Expectation on WidgetTester {
);
expect(icon, findsOneWidget);
} else if (type == FlowyIconType.custom) {
final isSvg = data.emoji.endsWith('.svg');
if (isURL(data.emoji)) {
final image = find.descendant(
of: pageName,
matching: isSvg
? find.byType(FlowyNetworkSvg)
: find.byType(FlowyNetworkImage),
matching: find.byType(FlowyNetworkImage),
);
expect(image, findsOneWidget);
} else {
final image = find.descendant(
of: pageName,
matching: isSvg ? find.byType(SvgPicture) : find.byType(Image),
matching: find.byType(Image),
);
expect(image, findsOneWidget);
}
@@ -294,26 +290,16 @@ extension Expectation on WidgetTester {
);
expect(icon, findsOneWidget);
} else if (type == FlowyIconType.custom) {
final isSvg = data.emoji.endsWith('.svg');
if (isURL(data.emoji)) {
final image = find.descendant(
of: find.byType(ViewTitleBar),
matching: isSvg
? find.byType(FlowyNetworkSvg)
: find.byType(FlowyNetworkImage),
matching: find.byType(FlowyNetworkImage),
);
expect(image, findsOneWidget);
} else {
final image = find.descendant(
of: find.byType(ViewTitleBar),
matching: isSvg
? find.byWidgetPredicate((w) {
if (w is! SvgPicture) return false;
final loader = w.bytesLoader;
if (loader is! SvgFileLoader) return false;
return loader.file.path.endsWith('.svg');
})
: find.byType(Image),
matching: find.byType(Image),
);
expect(image, findsOneWidget);
}
@@ -0,0 +1,81 @@
import 'dart:async';
import 'dart:convert';
import 'package:appflowy/ai/service/error.dart';
import 'package:appflowy/ai/service/openai_client.dart';
import 'package:appflowy/ai/service/text_completion.dart';
import 'package:http/http.dart' as http;
import 'package:mocktail/mocktail.dart';
class MyMockClient extends Mock implements http.Client {
@override
Future<http.StreamedResponse> send(http.BaseRequest request) async {
final requestType = request.method;
final requestUri = request.url;
if (requestType == 'POST' &&
requestUri == OpenAIRequestType.textCompletion.uri) {
final responseHeaders = <String, String>{
'content-type': 'text/event-stream',
};
final responseBody = Stream.fromIterable([
utf8.encode(
'{ "choices": [{"text": "Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula ", "index": 0, "logprobs": null, "finish_reason": null}]}',
),
utf8.encode('\n'),
utf8.encode('[DONE]'),
]);
// Return a mocked response with the expected data
return http.StreamedResponse(responseBody, 200, headers: responseHeaders);
}
// Return an error response for any other request
return http.StreamedResponse(const Stream.empty(), 404);
}
}
class MockOpenAIRepository extends HttpOpenAIRepository {
MockOpenAIRepository() : super(apiKey: 'dummyKey', client: MyMockClient());
@override
Future<void> getStreamedCompletions({
required String prompt,
required Future<void> Function() onStart,
required Future<void> Function(TextCompletionResponse response) onProcess,
required Future<void> Function() onEnd,
required void Function(AIError error) onError,
String? suffix,
int maxTokens = 2048,
double temperature = 0.3,
bool useAction = false,
}) async {
final request = http.Request('POST', OpenAIRequestType.textCompletion.uri);
final response = await client.send(request);
String previousSyntax = '';
if (response.statusCode == 200) {
await for (final chunk in response.stream
.transform(const Utf8Decoder())
.transform(const LineSplitter())) {
await onStart();
final data = chunk.trim().split('data: ');
if (data[0] != '[DONE]') {
final response = TextCompletionResponse.fromJson(
json.decode(data[0]),
);
if (response.choices.isNotEmpty) {
final text = response.choices.first.text;
if (text == previousSyntax && text == '\n') {
continue;
}
await onProcess(response);
previousSyntax = response.choices.first.text;
}
} else {
await onEnd();
}
}
}
}
}
+11 -18
View File
@@ -1,5 +1,5 @@
PODS:
- app_links (0.0.2):
- app_links (0.0.1):
- Flutter
- appflowy_backend (0.0.1):
- Flutter
@@ -66,8 +66,6 @@ PODS:
- permission_handler_apple (9.3.0):
- Flutter
- ReachabilitySwift (5.0.0)
- saver_gallery (0.0.1):
- Flutter
- SDWebImage (5.14.2):
- SDWebImage/Core (= 5.14.2)
- SDWebImage/Core (5.14.2)
@@ -81,7 +79,7 @@ PODS:
- shared_preferences_foundation (0.0.1):
- Flutter
- FlutterMacOS
- sqflite_darwin (0.0.4):
- sqflite (0.0.3):
- Flutter
- FlutterMacOS
- super_native_extensions (0.0.1):
@@ -92,7 +90,6 @@ PODS:
- Flutter
- webview_flutter_wkwebview (0.0.1):
- Flutter
- FlutterMacOS
DEPENDENCIES:
- app_links (from `.symlinks/plugins/app_links/ios`)
@@ -111,14 +108,13 @@ DEPENDENCIES:
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
- saver_gallery (from `.symlinks/plugins/saver_gallery/ios`)
- sentry_flutter (from `.symlinks/plugins/sentry_flutter/ios`)
- share_plus (from `.symlinks/plugins/share_plus/ios`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
- sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`)
- sqflite (from `.symlinks/plugins/sqflite/darwin`)
- super_native_extensions (from `.symlinks/plugins/super_native_extensions/ios`)
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
- webview_flutter_wkwebview (from `.symlinks/plugins/webview_flutter_wkwebview/darwin`)
- webview_flutter_wkwebview (from `.symlinks/plugins/webview_flutter_wkwebview/ios`)
SPEC REPOS:
trunk:
@@ -163,25 +159,23 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/path_provider_foundation/darwin"
permission_handler_apple:
:path: ".symlinks/plugins/permission_handler_apple/ios"
saver_gallery:
:path: ".symlinks/plugins/saver_gallery/ios"
sentry_flutter:
:path: ".symlinks/plugins/sentry_flutter/ios"
share_plus:
:path: ".symlinks/plugins/share_plus/ios"
shared_preferences_foundation:
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
sqflite_darwin:
:path: ".symlinks/plugins/sqflite_darwin/darwin"
sqflite:
:path: ".symlinks/plugins/sqflite/darwin"
super_native_extensions:
:path: ".symlinks/plugins/super_native_extensions/ios"
url_launcher_ios:
:path: ".symlinks/plugins/url_launcher_ios/ios"
webview_flutter_wkwebview:
:path: ".symlinks/plugins/webview_flutter_wkwebview/darwin"
:path: ".symlinks/plugins/webview_flutter_wkwebview/ios"
SPEC CHECKSUMS:
app_links: 3da4c36b46cac3bf24eb897f1a6ce80bda109874
app_links: c5161ac5ab5383ad046884568b4b91cb52df5d91
appflowy_backend: 78f6a053f756e6bc29bcc5a2106cbe77b756e97a
connectivity_plus: 481668c94744c30c53b8895afb39159d1e619bdf
device_info_plus: 71ffc6ab7634ade6267c7a93088ed7e4f74e5896
@@ -192,7 +186,7 @@ SPEC CHECKSUMS:
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
fluttertoast: 76fea30fcf04176325f6864c87306927bd7d2038
image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a
integration_test: 4a889634ef21a45d28d50d622cf412dc6d9f586e
integration_test: d5929033778cc4991a187e4e1a85396fa4f59b3a
irondash_engine_context: 8e58ca8e0212ee9d1c7dc6a42121849986c88486
keyboard_height_plugin: ef70a8181b29f27670e9e2450814ca6b6dc05b05
open_filex: 432f3cd11432da3e39f47fcc0df2b1603854eff1
@@ -200,18 +194,17 @@ SPEC CHECKSUMS:
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d
ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825
saver_gallery: af2d0c762dafda254e0ad025ef0dabd6506cd490
SDWebImage: b9a731e1d6307f44ca703b3976d18c24ca561e84
Sentry: 1fe34e9c2cbba1e347623610d26db121dcb569f1
sentry_flutter: e24b397f9a61fa5bbefd8279c3b2242ca86faa90
share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a
shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
sqflite: c35dad70033b8862124f8337cc994a809fcd9fa3
super_native_extensions: b763c02dc3a8fd078389f410bf15149179020cb4
SwiftyGif: 6c3eafd0ce693cad58bb63d2b2fb9bacb8552780
Toast: 91b396c56ee72a5790816f40d3a94dd357abc196
url_launcher_ios: 694010445543906933d732453a59da0a173ae33d
webview_flutter_wkwebview: 44d4dee7d7056d5ad185d25b38404436d56c547c
webview_flutter_wkwebview: 45a041c7831641076618876de3ba75c712860c6b
PODFILE CHECKSUM: d0d9b4ff572d8695c38eb3f9b490f55cdfc57eca
@@ -1,7 +1,7 @@
import UIKit
import Flutter
@main
@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
+69 -72
View File
@@ -1,78 +1,75 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true />
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleLocalizations</key>
<array>
<string>en</string>
</array>
<key>CFBundleName</key>
<string>AppFlowy</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLName</key>
<string></string>
<key>CFBundleURLSchemes</key>
<array>
<string>appflowy-flutter</string>
</array>
</dict>
</array>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>FLTEnableImpeller</key>
<false />
<key>LSRequiresIPhoneOS</key>
<true />
<key>NSAppTransportSecurity</key>
<dict>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleLocalizations</key>
<array>
<string>en</string>
</array>
<key>CFBundleName</key>
<string>AppFlowy</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true />
<key>CFBundleURLName</key>
<string></string>
<key>CFBundleURLSchemes</key>
<array>
<string>appflowy-flutter</string>
</array>
</dict>
<key>NSPhotoLibraryUsageDescription</key>
<string>AppFlowy needs access to your photos to let you add images to your documents</string>
<key>NSPhotoLibraryAddUsageDescription</key>
<string>AppFlowy needs access to your photos to let you add images to your photo library</string>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true />
<key>NSCameraUsageDescription</key>
<string>AppFlowy needs access to your camera to let you add images to your documents from
camera</string>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
</array>
<key>UISupportsDocumentBrowser</key>
<true />
<key>UIViewControllerBasedStatusBarAppearance</key>
<false />
</array>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>FLTEnableImpeller</key>
<false/>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
</plist>
<key>NSPhotoLibraryUsageDescription</key>
<string>AppFlowy needs access to your photos to let you add images to your documents</string>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>NSCameraUsageDescription</key>
<string>AppFlowy needs access to your camera to let you add images to your documents from camera</string>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
</array>
<key>UISupportsDocumentBrowser</key>
<true/>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
</dict>
</plist>
+1 -5
View File
@@ -1,10 +1,6 @@
export 'service/ai_entities.dart';
export 'service/ai_prompt_input_bloc.dart';
export 'service/appflowy_ai_service.dart';
export 'service/error.dart';
export 'widgets/loading_indicator.dart';
export 'widgets/prompt_input/action_buttons.dart';
export 'widgets/prompt_input/desktop_prompt_text_field.dart';
export 'widgets/prompt_input/desktop_input_text_field.dart';
export 'widgets/prompt_input/file_attachment_list.dart';
export 'widgets/prompt_input/layout_define.dart';
export 'widgets/prompt_input/mention_page_bottom_sheet.dart';
@@ -0,0 +1,34 @@
import 'package:appflowy_backend/protobuf/flowy-ai/protobuf.dart';
import 'package:appflowy_result/appflowy_result.dart';
import 'error.dart';
import 'text_completion.dart';
abstract class AIRepository {
Future<void> getStreamedCompletions({
required String prompt,
required Future<void> Function() onStart,
required Future<void> Function(TextCompletionResponse response) onProcess,
required Future<void> Function() onEnd,
required void Function(AIError error) onError,
String? suffix,
int maxTokens = 2048,
double temperature = 0.3,
bool useAction = false,
});
Future<void> streamCompletion({
String? objectId,
required String text,
required CompletionTypePB completionType,
required Future<void> Function() onStart,
required Future<void> Function(String text) onProcess,
required Future<void> Function() onEnd,
required void Function(AIError error) onError,
});
Future<FlowyResult<List<String>, AIError>> generateImage({
required String prompt,
int n = 1,
});
}
@@ -1,85 +0,0 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy_backend/protobuf/flowy-ai/protobuf.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:equatable/equatable.dart';
class PredefinedFormat extends Equatable {
const PredefinedFormat({
required this.imageFormat,
required this.textFormat,
});
final ImageFormat imageFormat;
final TextFormat? textFormat;
PredefinedFormatPB toPB() {
return PredefinedFormatPB(
imageFormat: switch (imageFormat) {
ImageFormat.text => ResponseImageFormatPB.TextOnly,
ImageFormat.image => ResponseImageFormatPB.ImageOnly,
ImageFormat.textAndImage => ResponseImageFormatPB.TextAndImage,
},
textFormat: switch (textFormat) {
TextFormat.paragraph => ResponseTextFormatPB.Paragraph,
TextFormat.bulletList => ResponseTextFormatPB.BulletedList,
TextFormat.numberedList => ResponseTextFormatPB.NumberedList,
TextFormat.table => ResponseTextFormatPB.Table,
_ => null,
},
);
}
@override
List<Object?> get props => [imageFormat, textFormat];
}
enum ImageFormat {
text,
image,
textAndImage;
bool get hasText => this == text || this == textAndImage;
FlowySvgData get icon {
return switch (this) {
ImageFormat.text => FlowySvgs.ai_text_s,
ImageFormat.image => FlowySvgs.ai_image_s,
ImageFormat.textAndImage => FlowySvgs.ai_text_image_s,
};
}
String get i18n {
return switch (this) {
ImageFormat.text => LocaleKeys.chat_changeFormat_textOnly.tr(),
ImageFormat.image => LocaleKeys.chat_changeFormat_imageOnly.tr(),
ImageFormat.textAndImage =>
LocaleKeys.chat_changeFormat_textAndImage.tr(),
};
}
}
enum TextFormat {
paragraph,
bulletList,
numberedList,
table;
FlowySvgData get icon {
return switch (this) {
TextFormat.paragraph => FlowySvgs.ai_paragraph_s,
TextFormat.bulletList => FlowySvgs.ai_list_s,
TextFormat.numberedList => FlowySvgs.ai_number_list_s,
TextFormat.table => FlowySvgs.ai_table_s,
};
}
String get i18n {
return switch (this) {
TextFormat.paragraph => LocaleKeys.chat_changeFormat_text.tr(),
TextFormat.bulletList => LocaleKeys.chat_changeFormat_bullet.tr(),
TextFormat.numberedList => LocaleKeys.chat_changeFormat_number.tr(),
TextFormat.table => LocaleKeys.chat_changeFormat_table.tr(),
};
}
}
@@ -3,109 +3,137 @@ import 'dart:ffi';
import 'dart:isolate';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/shared/list_extension.dart';
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-ai/entities.pb.dart';
import 'package:appflowy_result/appflowy_result.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:fixnum/fixnum.dart' as fixnum;
import 'ai_entities.dart';
import 'ai_client.dart';
import 'error.dart';
import 'text_completion.dart';
abstract class AIRepository {
Future<void> streamCompletion({
String? objectId,
required String text,
PredefinedFormat? format,
required CompletionTypePB completionType,
required Future<void> Function() onStart,
required Future<void> Function(String text) onProcess,
required Future<void> Function() onEnd,
required void Function(AIError error) onError,
});
enum AskAIAction {
summarize,
fixSpelling,
improveWriting,
makeItLonger;
String get toInstruction => switch (this) {
summarize => 'Tl;dr',
fixSpelling => 'Correct this to standard English:',
improveWriting => 'Rewrite this in your own words:',
makeItLonger => 'Make this text longer:',
};
String prompt(String input) => switch (this) {
summarize => '$input\n\n$toInstruction',
_ => "$toInstruction\n\n$input",
};
static AskAIAction from(int index) => switch (index) {
0 => summarize,
1 => fixSpelling,
2 => improveWriting,
3 => makeItLonger,
_ => fixSpelling
};
String get name => switch (this) {
summarize => LocaleKeys.document_plugins_smartEditSummarize.tr(),
fixSpelling => LocaleKeys.document_plugins_smartEditFixSpelling.tr(),
improveWriting =>
LocaleKeys.document_plugins_smartEditImproveWriting.tr(),
makeItLonger => LocaleKeys.document_plugins_smartEditMakeLonger.tr(),
};
}
class AppFlowyAIService implements AIRepository {
@override
Future<(String, CompletionStream)?> streamCompletion({
Future<FlowyResult<List<String>, AIError>> generateImage({
required String prompt,
int n = 1,
}) {
throw UnimplementedError();
}
@override
Future<void> getStreamedCompletions({
required String prompt,
required Future<void> Function() onStart,
required Future<void> Function(TextCompletionResponse response) onProcess,
required Future<void> Function() onEnd,
required void Function(AIError error) onError,
String? suffix,
int maxTokens = 2048,
double temperature = 0.3,
bool useAction = false,
}) {
throw UnimplementedError();
}
@override
Future<CompletionStream> streamCompletion({
String? objectId,
required String text,
PredefinedFormat? format,
List<String> sourceIds = const [],
required CompletionTypePB completionType,
required Future<void> Function() onStart,
required Future<void> Function(String text) onProcess,
required Future<void> Function() onEnd,
required void Function(AIError error) onError,
}) async {
final stream = AppFlowyCompletionStream(
onStart: onStart,
onProcess: onProcess,
onEnd: onEnd,
onError: onError,
final stream = CompletionStream(
onStart,
onProcess,
onEnd,
onError,
);
final List<String> ragIds = [];
if (objectId != null) {
ragIds.add(objectId);
}
final payload = CompleteTextPB(
text: text,
completionType: completionType,
format: format?.toPB(),
streamPort: fixnum.Int64(stream.nativePort),
objectId: objectId ?? '',
ragIds: [
if (objectId != null) objectId,
...sourceIds,
].unique(),
objectId: objectId ?? "",
ragIds: ragIds,
);
return AIEventCompleteText(payload).send().fold(
(task) => (task.taskId, stream),
(error) {
Log.error(error);
return null;
},
);
// ignore: unawaited_futures
AIEventCompleteText(payload).send();
return stream;
}
}
abstract class CompletionStream {
CompletionStream({
required this.onStart,
required this.onProcess,
required this.onEnd,
required this.onError,
});
final Future<void> Function() onStart;
final Future<void> Function(String text) onProcess;
final Future<void> Function() onEnd;
final void Function(AIError error) onError;
CompletionTypePB completionTypeFromInt(AskAIAction action) {
switch (action) {
case AskAIAction.summarize:
return CompletionTypePB.MakeShorter;
case AskAIAction.fixSpelling:
return CompletionTypePB.SpellingAndGrammar;
case AskAIAction.improveWriting:
return CompletionTypePB.ImproveWriting;
case AskAIAction.makeItLonger:
return CompletionTypePB.MakeLonger;
}
}
class AppFlowyCompletionStream extends CompletionStream {
AppFlowyCompletionStream({
required super.onStart,
required super.onProcess,
required super.onEnd,
required super.onError,
}) {
_startListening();
}
final RawReceivePort _port = RawReceivePort();
final StreamController<String> _controller = StreamController.broadcast();
late StreamSubscription<String> _subscription;
int get nativePort => _port.sendPort.nativePort;
void _startListening() {
class CompletionStream {
CompletionStream(
Future<void> Function() onStart,
Future<void> Function(String text) onProcess,
Future<void> Function() onEnd,
void Function(AIError error) onError,
) {
_port.handler = _controller.add;
_subscription = _controller.stream.listen(
(event) async {
if (event == "AI_RESPONSE_LIMIT") {
onError(
AIError(
message: LocaleKeys.ai_textLimitReachedDescription.tr(),
message: LocaleKeys.sideBar_aiResponseLimit.tr(),
code: AIErrorCode.aiResponseLimitExceeded,
),
);
@@ -114,7 +142,7 @@ class AppFlowyCompletionStream extends CompletionStream {
if (event == "AI_IMAGE_RESPONSE_LIMIT") {
onError(
AIError(
message: LocaleKeys.ai_imageLimitReachedDescription.tr(),
message: LocaleKeys.sideBar_aiImageResponseLimit.tr(),
code: AIErrorCode.aiImageResponseLimitExceeded,
),
);
@@ -125,7 +153,6 @@ class AppFlowyCompletionStream extends CompletionStream {
onError(
AIError(
message: msg,
code: AIErrorCode.other,
),
);
}
@@ -143,17 +170,26 @@ class AppFlowyCompletionStream extends CompletionStream {
}
if (event.startsWith("error:")) {
onError(
AIError(message: event.substring(6), code: AIErrorCode.other),
);
onError(AIError(message: event.substring(6)));
}
},
);
}
final RawReceivePort _port = RawReceivePort();
final StreamController<String> _controller = StreamController.broadcast();
late StreamSubscription<String> _subscription;
int get nativePort => _port.sendPort.nativePort;
Future<void> dispose() async {
await _controller.close();
await _subscription.cancel();
_port.close();
}
StreamSubscription<String> listen(
void Function(String event)? onData,
) {
return _controller.stream.listen(onData);
}
}
@@ -7,7 +7,7 @@ part 'error.g.dart';
class AIError with _$AIError {
const factory AIError({
required String message,
required AIErrorCode code,
@Default(AIErrorCode.other) AIErrorCode code,
}) = _AIError;
factory AIError.fromJson(Map<String, Object?> json) =>
@@ -0,0 +1,173 @@
import 'dart:async';
import 'dart:convert';
import 'package:appflowy_backend/protobuf/flowy-ai/entities.pbenum.dart';
import 'package:appflowy_result/appflowy_result.dart';
import 'package:http/http.dart' as http;
import 'ai_client.dart';
import 'error.dart';
import 'text_completion.dart';
enum OpenAIRequestType {
textCompletion,
textEdit,
imageGenerations;
Uri get uri {
switch (this) {
case OpenAIRequestType.textCompletion:
return Uri.parse('https://api.openai.com/v1/completions');
case OpenAIRequestType.textEdit:
return Uri.parse('https://api.openai.com/v1/chat/completions');
case OpenAIRequestType.imageGenerations:
return Uri.parse('https://api.openai.com/v1/images/generations');
}
}
}
class HttpOpenAIRepository implements AIRepository {
const HttpOpenAIRepository({
required this.client,
required this.apiKey,
});
final http.Client client;
final String apiKey;
Map<String, String> get headers => {
'Authorization': 'Bearer $apiKey',
'Content-Type': 'application/json',
};
@override
Future<void> getStreamedCompletions({
required String prompt,
required Future<void> Function() onStart,
required Future<void> Function(TextCompletionResponse response) onProcess,
required Future<void> Function() onEnd,
required void Function(AIError error) onError,
String? suffix,
int maxTokens = 2048,
double temperature = 0.3,
bool useAction = false,
}) async {
final parameters = {
'model': 'gpt-3.5-turbo-instruct',
'prompt': prompt,
'suffix': suffix,
'max_tokens': maxTokens,
'temperature': temperature,
'stream': true,
};
final request = http.Request('POST', OpenAIRequestType.textCompletion.uri);
request.headers.addAll(headers);
request.body = jsonEncode(parameters);
final response = await client.send(request);
// NEED TO REFACTOR.
// WHY OPENAI USE TWO LINES TO INDICATE THE START OF THE STREAMING RESPONSE?
// AND WHY OPENAI USE [DONE] TO INDICATE THE END OF THE STREAMING RESPONSE?
int syntax = 0;
var previousSyntax = '';
if (response.statusCode == 200) {
await for (final chunk in response.stream
.transform(const Utf8Decoder())
.transform(const LineSplitter())) {
syntax += 1;
if (!useAction) {
if (syntax == 3) {
await onStart();
continue;
} else if (syntax < 3) {
continue;
}
} else {
if (syntax == 2) {
await onStart();
continue;
} else if (syntax < 2) {
continue;
}
}
final data = chunk.trim().split('data: ');
if (data.length > 1) {
if (data[1] != '[DONE]') {
final response = TextCompletionResponse.fromJson(
json.decode(data[1]),
);
if (response.choices.isNotEmpty) {
final text = response.choices.first.text;
if (text == previousSyntax && text == '\n') {
continue;
}
await onProcess(response);
previousSyntax = response.choices.first.text;
}
} else {
await onEnd();
}
}
}
} else {
final body = await response.stream.bytesToString();
onError(
AIError.fromJson(json.decode(body)['error']),
);
}
return;
}
@override
Future<FlowyResult<List<String>, AIError>> generateImage({
required String prompt,
int n = 1,
}) async {
final parameters = {
'prompt': prompt,
'n': n,
'size': '512x512',
};
try {
final response = await client.post(
OpenAIRequestType.imageGenerations.uri,
headers: headers,
body: json.encode(parameters),
);
if (response.statusCode == 200) {
final data = json.decode(
utf8.decode(response.bodyBytes),
)['data'] as List;
final urls = data
.map((e) => e.values)
.expand((e) => e)
.map((e) => e.toString())
.toList();
return FlowyResult.success(urls);
} else {
return FlowyResult.failure(
AIError.fromJson(json.decode(response.body)['error']),
);
}
} catch (error) {
return FlowyResult.failure(AIError(message: error.toString()));
}
}
@override
Future<void> streamCompletion({
String? objectId,
required String text,
required CompletionTypePB completionType,
required Future<void> Function() onStart,
required Future<void> Function(String text) onProcess,
required Future<void> Function() onEnd,
required void Function(AIError error) onError,
}) {
throw UnimplementedError();
}
}
@@ -0,0 +1,27 @@
import 'package:freezed_annotation/freezed_annotation.dart';
part 'text_completion.freezed.dart';
part 'text_completion.g.dart';
@freezed
class TextCompletionChoice with _$TextCompletionChoice {
factory TextCompletionChoice({
required String text,
required int index,
// ignore: invalid_annotation_target
@JsonKey(name: 'finish_reason') String? finishReason,
}) = _TextCompletionChoice;
factory TextCompletionChoice.fromJson(Map<String, Object?> json) =>
_$TextCompletionChoiceFromJson(json);
}
@freezed
class TextCompletionResponse with _$TextCompletionResponse {
const factory TextCompletionResponse({
required List<TextCompletionChoice> choices,
}) = _TextCompletionResponse;
factory TextCompletionResponse.fromJson(Map<String, Object?> json) =>
_$TextCompletionResponseFromJson(json);
}
@@ -16,48 +16,51 @@ class AILoadingIndicator extends StatelessWidget {
@override
Widget build(BuildContext context) {
final slice = Duration(milliseconds: duration.inMilliseconds ~/ 5);
return SizedBox(
height: 20,
child: SeparatedRow(
separatorBuilder: () => const HSpace(4),
children: [
Padding(
padding: const EdgeInsetsDirectional.only(end: 4.0),
child: FlowyText(
text,
color: Theme.of(context).hintColor,
return Padding(
padding: const EdgeInsets.only(top: 8.0),
child: SizedBox(
height: 20,
child: SeparatedRow(
separatorBuilder: () => const HSpace(4),
children: [
Padding(
padding: const EdgeInsetsDirectional.only(end: 4.0),
child: FlowyText(
text,
color: Theme.of(context).hintColor,
),
),
),
buildDot(const Color(0xFF9327FF))
.animate(onPlay: (controller) => controller.repeat())
.slideY(duration: slice, begin: 0, end: -1)
.then()
.slideY(begin: -1, end: 1)
.then()
.slideY(begin: 1, end: 0)
.then()
.slideY(duration: slice * 2, begin: 0, end: 0),
buildDot(const Color(0xFFFB006D))
.animate(onPlay: (controller) => controller.repeat())
.slideY(duration: slice, begin: 0, end: 0)
.then()
.slideY(begin: 0, end: -1)
.then()
.slideY(begin: -1, end: 1)
.then()
.slideY(begin: 1, end: 0)
.then()
.slideY(begin: 0, end: 0),
buildDot(const Color(0xFFFFCE00))
.animate(onPlay: (controller) => controller.repeat())
.slideY(duration: slice * 2, begin: 0, end: 0)
.then()
.slideY(duration: slice, begin: 0, end: -1)
.then()
.slideY(begin: -1, end: 1)
.then()
.slideY(begin: 1, end: 0),
],
buildDot(const Color(0xFF9327FF))
.animate(onPlay: (controller) => controller.repeat())
.slideY(duration: slice, begin: 0, end: -1)
.then()
.slideY(begin: -1, end: 1)
.then()
.slideY(begin: 1, end: 0)
.then()
.slideY(duration: slice * 2, begin: 0, end: 0),
buildDot(const Color(0xFFFB006D))
.animate(onPlay: (controller) => controller.repeat())
.slideY(duration: slice, begin: 0, end: 0)
.then()
.slideY(begin: 0, end: -1)
.then()
.slideY(begin: -1, end: 1)
.then()
.slideY(begin: 1, end: 0)
.then()
.slideY(begin: 0, end: 0),
buildDot(const Color(0xFFFFCE00))
.animate(onPlay: (controller) => controller.repeat())
.slideY(duration: slice * 2, begin: 0, end: 0)
.then()
.slideY(duration: slice, begin: 0, end: -1)
.then()
.slideY(begin: -1, end: 1)
.then()
.slideY(begin: 1, end: 0),
],
),
),
);
}
@@ -0,0 +1,53 @@
import 'package:appflowy/plugins/ai_chat/application/chat_input_control_cubit.dart';
import 'package:appflowy/plugins/ai_chat/presentation/layout_define.dart';
import 'package:extended_text_field/extended_text_field.dart';
import 'package:flutter/material.dart';
import 'mentioned_page_text_span.dart';
class PromptInputTextField extends StatelessWidget {
const PromptInputTextField({
super.key,
required this.cubit,
required this.textController,
required this.textFieldFocusNode,
required this.contentPadding,
this.hintText = "",
});
final ChatInputControlCubit cubit;
final TextEditingController textController;
final FocusNode textFieldFocusNode;
final EdgeInsetsGeometry contentPadding;
final String hintText;
@override
Widget build(BuildContext context) {
return ExtendedTextField(
controller: textController,
focusNode: textFieldFocusNode,
decoration: InputDecoration(
border: InputBorder.none,
enabledBorder: InputBorder.none,
focusedBorder: InputBorder.none,
contentPadding: contentPadding,
hintText: hintText,
hintStyle: AIChatUILayout.inputHintTextStyle(context),
isCollapsed: true,
isDense: true,
),
keyboardType: TextInputType.multiline,
textCapitalization: TextCapitalization.sentences,
minLines: 1,
maxLines: null,
style: Theme.of(context).textTheme.bodyMedium,
specialTextSpanBuilder: PromptInputTextSpanBuilder(
inputControlCubit: cubit,
specialTextStyle: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.primary,
fontWeight: FontWeight.w600,
),
),
);
}
}
@@ -1,5 +1,5 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/ai/service/ai_prompt_input_bloc.dart';
import 'package:appflowy/plugins/ai_chat/application/ai_prompt_input_bloc.dart';
import 'package:appflowy/plugins/ai_chat/application/chat_entity.dart';
import 'package:appflowy/plugins/ai_chat/application/chat_input_file_bloc.dart';
import 'package:flowy_infra/theme_extension.dart';
@@ -13,9 +13,11 @@ import 'layout_define.dart';
class PromptInputFile extends StatelessWidget {
const PromptInputFile({
super.key,
required this.chatId,
required this.onDeleted,
});
final String chatId;
final void Function(ChatFile) onDeleted;
@override
@@ -35,6 +37,7 @@ class PromptInputFile extends StatelessWidget {
),
itemCount: files.length,
itemBuilder: (context, index) => ChatFilePreview(
chatId: chatId,
file: files[index],
onDeleted: () => onDeleted(files[index]),
),
@@ -46,11 +49,13 @@ class PromptInputFile extends StatelessWidget {
class ChatFilePreview extends StatefulWidget {
const ChatFilePreview({
required this.chatId,
required this.file,
required this.onDeleted,
super.key,
});
final String chatId;
final ChatFile file;
final VoidCallback onDeleted;
@@ -49,9 +49,7 @@ class _PromptInputMentionPageMenuState
void initState() {
super.initState();
Future.delayed(Duration.zero, () {
if (mounted) {
context.read<ChatInputControlCubit>().refreshViews();
}
context.read<ChatInputControlCubit>().refreshViews();
});
}
@@ -1,22 +1,24 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/ai_chat/application/chat_entity.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/style_widget/hover.dart';
import 'package:flutter/material.dart';
import 'package:universal_platform/universal_platform.dart';
import '../../service/ai_entities.dart';
import 'layout_define.dart';
class PromptInputDesktopToggleFormatButton extends StatelessWidget {
const PromptInputDesktopToggleFormatButton({
super.key,
required this.showFormatBar,
required this.predefinedFormat,
required this.onTap,
});
final bool showFormatBar;
final PredefinedFormat predefinedFormat;
final VoidCallback onTap;
@override
@@ -67,7 +69,7 @@ class ChangeFormatBar extends StatelessWidget {
_buildFormatButton(context, ImageFormat.image),
if (predefinedFormat?.imageFormat.hasText ?? true) ...[
_buildDivider(),
_buildTextFormatButton(context, TextFormat.paragraph),
_buildTextFormatButton(context, TextFormat.auto),
_buildTextFormatButton(context, TextFormat.bulletList),
_buildTextFormatButton(context, TextFormat.numberedList),
_buildTextFormatButton(context, TextFormat.table),
@@ -86,8 +88,7 @@ class ChangeFormatBar extends StatelessWidget {
return;
}
if (format.hasText) {
final textFormat =
predefinedFormat?.textFormat ?? TextFormat.paragraph;
final textFormat = predefinedFormat?.textFormat ?? TextFormat.auto;
onSelectPredefinedFormat(
PredefinedFormat(imageFormat: format, textFormat: textFormat),
);
@@ -2,6 +2,7 @@ import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/base/flowy_search_text_field.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
import 'package:appflowy/plugins/ai_chat/application/chat_bloc.dart';
import 'package:appflowy/plugins/ai_chat/application/chat_select_sources_cubit.dart';
import 'package:appflowy/plugins/base/drag_handler.dart';
import 'package:appflowy/workspace/application/sidebar/space/space_bloc.dart';
@@ -18,11 +19,9 @@ import 'select_sources_menu.dart';
class PromptInputMobileSelectSourcesButton extends StatefulWidget {
const PromptInputMobileSelectSourcesButton({
super.key,
required this.selectedSourcesNotifier,
required this.onUpdateSelectedSources,
});
final ValueNotifier<List<String>> selectedSourcesNotifier;
final void Function(List<String>) onUpdateSelectedSources;
@override
@@ -37,15 +36,15 @@ class _PromptInputMobileSelectSourcesButtonState
@override
void initState() {
super.initState();
widget.selectedSourcesNotifier.addListener(onSelectedSourcesChanged);
WidgetsBinding.instance.addPostFrameCallback((_) {
onSelectedSourcesChanged();
cubit.updateSelectedSources(
context.read<ChatBloc>().state.selectedSourceIds,
);
});
}
@override
void dispose() {
widget.selectedSourcesNotifier.removeListener(onSelectedSourcesChanged);
cubit.close();
super.dispose();
}
@@ -71,49 +70,56 @@ class _PromptInputMobileSelectSourcesButtonState
],
child: BlocBuilder<SpaceBloc, SpaceState>(
builder: (context, state) {
return FlowyButton(
margin: const EdgeInsetsDirectional.fromSTEB(4, 6, 2, 6),
expandText: false,
text: Row(
mainAxisSize: MainAxisSize.min,
children: [
FlowySvg(
FlowySvgs.ai_page_s,
color: Theme.of(context).iconTheme.color,
size: const Size.square(20.0),
),
FlowySvg(
FlowySvgs.ai_source_drop_down_s,
color: Theme.of(context).hintColor,
size: const Size.square(10),
),
],
),
onTap: () async {
context
.read<ChatSettingsCubit>()
.refreshSources(state.spaces, state.currentSpace);
await showMobileBottomSheet<void>(
context,
backgroundColor: Theme.of(context).colorScheme.surface,
maxChildSize: 0.98,
enableDraggableScrollable: true,
scrollableWidgetBuilder: (_, scrollController) {
return Expanded(
child: BlocProvider.value(
value: cubit,
child: _MobileSelectSourcesSheetBody(
scrollController: scrollController,
),
),
);
},
builder: (context) => const SizedBox.shrink(),
);
if (context.mounted) {
widget.onUpdateSelectedSources(cubit.selectedSourceIds);
}
return BlocListener<ChatBloc, ChatState>(
listener: (context, state) {
cubit
..updateSelectedSources(state.selectedSourceIds)
..updateSelectedStatus();
},
child: FlowyButton(
margin: const EdgeInsetsDirectional.fromSTEB(4, 6, 2, 6),
expandText: false,
text: Row(
mainAxisSize: MainAxisSize.min,
children: [
FlowySvg(
FlowySvgs.ai_page_s,
color: Theme.of(context).iconTheme.color,
size: const Size.square(20.0),
),
FlowySvg(
FlowySvgs.ai_source_drop_down_s,
color: Theme.of(context).hintColor,
size: const Size.square(10),
),
],
),
onTap: () async {
context
.read<ChatSettingsCubit>()
.refreshSources(state.spaces, state.currentSpace);
await showMobileBottomSheet<void>(
context,
backgroundColor: Theme.of(context).colorScheme.surface,
maxChildSize: 0.98,
enableDraggableScrollable: true,
scrollableWidgetBuilder: (_, scrollController) {
return Expanded(
child: BlocProvider.value(
value: cubit,
child: _MobileSelectSourcesSheetBody(
scrollController: scrollController,
),
),
);
},
builder: (context) => const SizedBox.shrink(),
);
if (context.mounted) {
widget.onUpdateSelectedSources(cubit.selectedSourceIds);
}
},
),
);
},
),
@@ -121,12 +127,6 @@ class _PromptInputMobileSelectSourcesButtonState
},
);
}
void onSelectedSourcesChanged() {
cubit
..updateSelectedSources(widget.selectedSourcesNotifier.value)
..updateSelectedStatus();
}
}
class _MobileSelectSourcesSheetBody extends StatefulWidget {
@@ -2,8 +2,8 @@ import 'dart:math';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/ai_chat/application/chat_bloc.dart';
import 'package:appflowy/plugins/ai_chat/application/chat_select_sources_cubit.dart';
import 'package:appflowy/plugins/document/application/document_bloc.dart';
import 'package:appflowy/workspace/application/sidebar/space/space_bloc.dart';
import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart';
import 'package:appflowy/workspace/application/view/view_ext.dart';
@@ -24,11 +24,9 @@ import 'mention_page_menu.dart';
class PromptInputDesktopSelectSourcesButton extends StatefulWidget {
const PromptInputDesktopSelectSourcesButton({
super.key,
required this.selectedSourcesNotifier,
required this.onUpdateSelectedSources,
});
final ValueNotifier<List<String>> selectedSourcesNotifier;
final void Function(List<String>) onUpdateSelectedSources;
@override
@@ -44,15 +42,15 @@ class _PromptInputDesktopSelectSourcesButtonState
@override
void initState() {
super.initState();
widget.selectedSourcesNotifier.addListener(onSelectedSourcesChanged);
WidgetsBinding.instance.addPostFrameCallback((_) {
onSelectedSourcesChanged();
cubit.updateSelectedSources(
context.read<ChatBloc>().state.selectedSourceIds,
);
});
}
@override
void dispose() {
widget.selectedSourcesNotifier.removeListener(onSelectedSourcesChanged);
cubit.close();
super.dispose();
}
@@ -78,53 +76,51 @@ class _PromptInputDesktopSelectSourcesButtonState
],
child: BlocBuilder<SpaceBloc, SpaceState>(
builder: (context, state) {
return AppFlowyPopover(
constraints: BoxConstraints.loose(const Size(320, 380)),
offset: const Offset(0.0, -10.0),
direction: PopoverDirection.topWithCenterAligned,
margin: EdgeInsets.zero,
controller: popoverController,
onOpen: () {
context
.read<ChatSettingsCubit>()
.refreshSources(state.spaces, state.currentSpace);
return BlocListener<ChatBloc, ChatState>(
listener: (context, state) {
cubit
..updateSelectedSources(state.selectedSourceIds)
..updateSelectedStatus();
},
onClose: () {
widget.onUpdateSelectedSources(cubit.selectedSourceIds);
context
.read<ChatSettingsCubit>()
.refreshSources(state.spaces, state.currentSpace);
},
popupBuilder: (_) {
return BlocProvider.value(
value: context.read<ChatSettingsCubit>(),
child: const _PopoverContent(),
);
},
child: _IndicatorButton(
selectedSourcesNotifier: widget.selectedSourcesNotifier,
onTap: () => popoverController.show(),
child: AppFlowyPopover(
constraints: BoxConstraints.loose(const Size(320, 380)),
offset: const Offset(0.0, -10.0),
direction: PopoverDirection.topWithCenterAligned,
margin: EdgeInsets.zero,
controller: popoverController,
onOpen: () {
context
.read<ChatSettingsCubit>()
.refreshSources(state.spaces, state.currentSpace);
},
onClose: () {
widget.onUpdateSelectedSources(cubit.selectedSourceIds);
context
.read<ChatSettingsCubit>()
.refreshSources(state.spaces, state.currentSpace);
},
popupBuilder: (_) {
return BlocProvider.value(
value: context.read<ChatSettingsCubit>(),
child: const _PopoverContent(),
);
},
child: _IndicatorButton(
onTap: () => popoverController.show(),
),
),
);
},
),
);
}
void onSelectedSourcesChanged() {
cubit
..updateSelectedSources(widget.selectedSourcesNotifier.value)
..updateSelectedStatus();
}
}
class _IndicatorButton extends StatelessWidget {
const _IndicatorButton({
required this.selectedSourcesNotifier,
required this.onTap,
});
final ValueNotifier<List<String>> selectedSourcesNotifier;
final VoidCallback onTap;
@override
@@ -148,19 +144,11 @@ class _IndicatorButton extends StatelessWidget {
color: Theme.of(context).iconTheme.color,
),
const HSpace(2.0),
ValueListenableBuilder(
valueListenable: selectedSourcesNotifier,
builder: (context, selectedSourceIds, _) {
final documentId =
context.read<DocumentBloc?>()?.documentId;
final label = documentId != null &&
selectedSourceIds.length == 1 &&
selectedSourceIds[0] == documentId
? LocaleKeys.chat_currentPage.tr()
: selectedSourceIds.length.toString();
BlocBuilder<ChatBloc, ChatState>(
builder: (context, state) {
return FlowyText(
label,
fontSize: 12,
state.selectedSourceIds.length.toString(),
fontSize: 14,
figmaLineHeight: 16,
color: Theme.of(context).hintColor,
);
@@ -115,9 +115,4 @@ class KVKeys {
///
/// The value is a json string of [RecentIcons]
static const String recentIcons = 'kRecentIcons';
/// The key for saving compact mode ids for node or databse view
///
/// The value is a json list of id
static const String compactModeIds = 'compactModeIds';
}
@@ -10,7 +10,6 @@ import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:open_filex/open_filex.dart';
import 'package:string_validator/string_validator.dart';
import 'package:universal_platform/universal_platform.dart';
import 'package:url_launcher/url_launcher.dart' as launcher;
typedef OnFailureCallback = void Function(Uri uri);
@@ -39,11 +38,6 @@ Future<bool> afLaunchUri(
);
}
// on Linux, add http scheme to the url if it is not present
if (UniversalPlatform.isLinux && !isURL(url, {'require_protocol': true})) {
uri = Uri.parse('https://$url');
}
// try to launch the uri directly
bool result;
try {
+1 -1
View File
@@ -180,7 +180,7 @@ Future<void> useLocalServer() async {
await _setAuthenticatorType(AuthenticatorType.local);
}
// Use getIt<AppFlowyCloudSharedEnv>() to get the shared environment.
/// Use getIt<AppFlowyCloudSharedEnv>() to get the shared environment.
class AppFlowyCloudSharedEnv {
AppFlowyCloudSharedEnv({
required AuthenticatorType authenticatorType,
@@ -502,11 +502,11 @@ class _AFDropdownMenuState<T> extends State<AFDropdownMenu<T>> {
// Simulate the focused state because the text field should always be focused
// during traversal. If the menu item has a custom foreground color, the "focused"
// color will also change to foregroundColor.withValues(alpha: 0.12).
// color will also change to foregroundColor.withOpacity(0.12).
effectiveStyle = entry.enabled && i == focusedIndex
? effectiveStyle.copyWith(
backgroundColor: WidgetStatePropertyAll<Color>(
focusedBackgroundColor.withValues(alpha: 0.12),
focusedBackgroundColor.withOpacity(0.12),
),
)
: effectiveStyle;
@@ -1,4 +1,3 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/application/base/mobile_view_page_bloc.dart';
import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart';
@@ -8,7 +7,6 @@ import 'package:appflowy/mobile/presentation/presentation.dart';
import 'package:appflowy/mobile/presentation/widgets/flowy_mobile_state_container.dart';
import 'package:appflowy/plugins/document/application/prelude.dart';
import 'package:appflowy/plugins/document/presentation/document_collaborators.dart';
import 'package:appflowy/plugins/document/presentation/editor_notification.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart';
import 'package:appflowy/shared/feature_flags.dart';
import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';
@@ -20,9 +18,6 @@ import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';
import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart';
import 'package:appflowy/workspace/application/view/view_bloc.dart';
import 'package:appflowy/workspace/application/view/view_ext.dart';
import 'package:appflowy/workspace/application/view/view_lock_status_bloc.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:appflowy/workspace/presentation/widgets/view_title_bar.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
@@ -96,7 +91,7 @@ class _MobileViewPageState extends State<MobileViewPage> {
final body = _buildBody(context, state);
if (view == null) {
return SizedBox.shrink();
return _buildApp(context, null, body);
}
return MultiBlocProvider(
@@ -127,11 +122,6 @@ class _MobileViewPageState extends State<MobileViewPage> {
create: (_) => DocumentPageStyleBloc(view: view)
..add(const DocumentPageStyleEvent.initial()),
),
if (view.layout.isDocumentView || view.layout.isDatabaseView)
BlocProvider(
create: (_) => ViewLockStatusBloc(view: view)
..add(const ViewLockStatusEvent.initial()),
),
],
child: Builder(
builder: (context) {
@@ -162,7 +152,6 @@ class _MobileViewPageState extends State<MobileViewPage> {
title: title,
appBarOpacity: _appBarOpacity,
actions: actions,
view: view,
)
: FlowyAppBar(title: title, actions: actions);
final body = isDocument
@@ -233,8 +222,6 @@ class _MobileViewPageState extends State<MobileViewPage> {
final isImmersiveMode =
context.read<MobileViewPageBloc>().state.isImmersiveMode;
final isLocked =
context.read<ViewLockStatusBloc?>()?.state.isLocked ?? false;
final actions = <Widget>[];
if (FeatureFlag.syncDocument.isOn) {
@@ -253,7 +240,7 @@ class _MobileViewPageState extends State<MobileViewPage> {
}
}
if (view.layout.isDocumentView && !isLocked) {
if (view.layout.isDocumentView) {
actions.addAll([
MobileViewPageLayoutButton(
view: view,
@@ -283,139 +270,25 @@ class _MobileViewPageState extends State<MobileViewPage> {
Widget _buildTitle(BuildContext context, ViewPB? view) {
final icon = view?.icon;
return ValueListenableBuilder(
valueListenable: _appBarOpacity,
builder: (_, value, child) {
if (value < 0.99) {
return Padding(
padding: const EdgeInsets.only(left: 6.0),
child: _buildLockStatus(context, view),
);
}
final name =
widget.fixedTitle ?? view?.nameOrDefault ?? widget.title ?? '';
return Opacity(
opacity: value,
child: Row(
children: [
if (icon != null && icon.value.isNotEmpty) ...[
RawEmojiIconWidget(
emoji: icon.toEmojiIconData(),
emojiSize: 15,
),
const HSpace(4),
],
Flexible(
child: FlowyText.medium(
name,
fontSize: 15.0,
overflow: TextOverflow.ellipsis,
figmaLineHeight: 18.0,
),
),
const HSpace(4.0),
_buildLockStatusIcon(context, view),
],
return Row(
mainAxisSize: MainAxisSize.min,
children: [
if (icon != null && icon.value.isNotEmpty) ...[
RawEmojiIconWidget(
emoji: icon.toEmojiIconData(),
emojiSize: 15,
),
);
},
);
}
Widget _buildLockStatus(BuildContext context, ViewPB? view) {
if (view == null || view.layout == ViewLayoutPB.Chat) {
return const SizedBox.shrink();
}
return BlocConsumer<ViewLockStatusBloc, ViewLockStatusState>(
listenWhen: (previous, current) =>
previous.isLoadingLockStatus == current.isLoadingLockStatus &&
current.isLoadingLockStatus == false,
listener: (context, state) {
if (state.isLocked) {
showToastNotification(
context,
message: LocaleKeys.lockPage_pageLockedToast.tr(),
);
EditorNotification.exitEditing().post();
}
},
builder: (context, state) {
if (state.isLocked) {
return LockedPageStatus();
} else if (!state.isLocked && state.lockCounter > 0) {
return ReLockedPageStatus();
}
return const SizedBox.shrink();
},
);
}
Widget _buildLockStatusIcon(BuildContext context, ViewPB? view) {
if (view == null || view.layout == ViewLayoutPB.Chat) {
return const SizedBox.shrink();
}
return BlocConsumer<ViewLockStatusBloc, ViewLockStatusState>(
listenWhen: (previous, current) =>
previous.isLoadingLockStatus == current.isLoadingLockStatus &&
current.isLoadingLockStatus == false,
listener: (context, state) {
if (state.isLocked) {
showToastNotification(
context,
message: LocaleKeys.lockPage_pageLockedToast.tr(),
);
}
},
builder: (context, state) {
if (state.isLocked) {
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
context.read<ViewLockStatusBloc>().add(
const ViewLockStatusEvent.unlock(),
);
},
child: Padding(
padding: const EdgeInsets.only(
top: 4.0,
right: 8,
bottom: 4.0,
),
child: FlowySvg(
FlowySvgs.lock_page_fill_s,
blendMode: null,
),
),
);
} else if (!state.isLocked && state.lockCounter > 0) {
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
context.read<ViewLockStatusBloc>().add(
const ViewLockStatusEvent.lock(),
);
},
child: Padding(
padding: const EdgeInsets.only(
top: 4.0,
right: 8,
bottom: 4.0,
),
child: FlowySvg(
FlowySvgs.unlock_page_s,
color: Color(0xFF8F959E),
blendMode: null,
),
),
);
}
return const SizedBox.shrink();
},
const HSpace(4),
],
Expanded(
child: FlowyText.medium(
widget.fixedTitle ?? view?.name ?? widget.title ?? '',
fontSize: 15.0,
overflow: TextOverflow.ellipsis,
figmaLineHeight: 18.0,
),
),
],
);
}
@@ -12,7 +12,6 @@ import 'package:appflowy/plugins/shared/share/share_bloc.dart';
import 'package:appflowy/shared/icon_emoji_picker/tab.dart';
import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';
import 'package:appflowy/workspace/application/view/prelude.dart';
import 'package:appflowy/workspace/application/view/view_lock_status_bloc.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/theme_extension.dart';
@@ -29,13 +28,12 @@ class MobileViewPageImmersiveAppBar extends StatelessWidget
required this.appBarOpacity,
required this.title,
required this.actions,
required this.view,
});
final ValueListenable appBarOpacity;
final Widget title;
final List<Widget> actions;
final ViewPB? view;
@override
final Size preferredSize;
@@ -45,9 +43,9 @@ class MobileViewPageImmersiveAppBar extends StatelessWidget
valueListenable: appBarOpacity,
builder: (_, opacity, __) => FlowyAppBar(
backgroundColor:
AppBarTheme.of(context).backgroundColor?.withValues(alpha: opacity),
AppBarTheme.of(context).backgroundColor?.withOpacity(opacity),
showDivider: false,
title: _buildTitle(context, opacity: opacity),
title: Opacity(opacity: opacity >= 0.99 ? 1.0 : 0, child: title),
leadingWidth: 44,
leading: Padding(
padding: const EdgeInsets.only(top: 4.0, bottom: 4.0, left: 12.0),
@@ -58,13 +56,6 @@ class MobileViewPageImmersiveAppBar extends StatelessWidget
);
}
Widget _buildTitle(
BuildContext context, {
required double opacity,
}) {
return title;
}
Widget _buildAppBarBackButton(BuildContext context) {
return AppBarButton(
padding: EdgeInsets.zero,
@@ -111,12 +102,6 @@ class MobileViewPageMoreButton extends StatelessWidget {
BlocProvider.value(value: context.read<FavoriteBloc>()),
BlocProvider.value(value: context.read<MobileViewPageBloc>()),
BlocProvider.value(value: context.read<ShareBloc>()),
BlocProvider(
create: (context) => ViewLockStatusBloc(view: view)
..add(
ViewLockStatusEvent.initial(),
),
),
],
child: MobileViewPageMoreBottomSheet(view: view),
),
@@ -239,7 +224,7 @@ class _ImmersiveAppBarButton extends StatelessWidget {
child = DecoratedBox(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(dimension / 2.0),
color: Colors.black.withValues(alpha: 0.2),
color: Colors.black.withOpacity(0.2),
),
child: child,
);
@@ -14,7 +14,6 @@ import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/util/string_extension.dart';
import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';
import 'package:appflowy/workspace/application/view/prelude.dart';
import 'package:appflowy/workspace/application/view/view_lock_status_bloc.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
@@ -44,8 +43,7 @@ class MobileViewPageMoreBottomSheet extends StatelessWidget {
},
child: ViewPageBottomSheet(
view: view,
onAction: (action, {arguments}) async =>
_onAction(context, action, arguments),
onAction: (action) async => _onAction(context, action),
onRename: (name) {
_onRename(context, name);
context.pop();
@@ -58,7 +56,6 @@ class MobileViewPageMoreBottomSheet extends StatelessWidget {
Future<void> _onAction(
BuildContext context,
MobileViewBottomSheetBodyAction action,
Map<String, dynamic>? arguments,
) async {
switch (action) {
case MobileViewBottomSheetBodyAction.duplicate:
@@ -110,32 +107,12 @@ class MobileViewPageMoreBottomSheet extends StatelessWidget {
break;
case MobileViewBottomSheetBodyAction.updatePathName:
_updatePathName(context);
case MobileViewBottomSheetBodyAction.lockPage:
final isLocked =
arguments?[MobileViewBottomSheetBodyActionArguments.isLockedKey] ??
false;
await _lockPage(context, isLocked: isLocked);
// context.pop();
break;
case MobileViewBottomSheetBodyAction.rename:
// no need to implement, rename is handled by the onRename callback.
throw UnimplementedError();
}
}
Future<void> _lockPage(
BuildContext context, {
required bool isLocked,
}) async {
if (isLocked) {
context.read<ViewLockStatusBloc>().add(const ViewLockStatusEvent.lock());
} else {
context
.read<ViewLockStatusBloc>()
.add(const ViewLockStatusEvent.unlock());
}
}
Future<void> _publish(BuildContext context) async {
final id = context.read<ShareBloc>().view.id;
final lastPublishName = context.read<ShareBloc>().state.pathName;
@@ -1,10 +1,8 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/widgets/widgets.dart';
import 'package:appflowy/workspace/application/view/view_lock_status_bloc.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
enum MobileViewItemBottomSheetBodyAction {
rename,
@@ -42,8 +40,6 @@ class MobileViewItemBottomSheetBody extends StatelessWidget {
BuildContext context,
MobileViewItemBottomSheetBodyAction action,
) {
final isLocked =
context.read<ViewLockStatusBloc?>()?.state.isLocked ?? false;
switch (action) {
case MobileViewItemBottomSheetBodyAction.rename:
return FlowyOptionTile.text(
@@ -53,7 +49,6 @@ class MobileViewItemBottomSheetBody extends StatelessWidget {
FlowySvgs.view_item_rename_s,
size: Size.square(18),
),
enable: !isLocked,
showTopBorder: false,
showBottomBorder: false,
onTap: () => onAction(
@@ -99,7 +94,6 @@ class MobileViewItemBottomSheetBody extends StatelessWidget {
size: const Size.square(18),
color: Theme.of(context).colorScheme.error,
),
enable: !isLocked,
showTopBorder: false,
showBottomBorder: false,
onTap: () => onAction(
@@ -4,12 +4,9 @@ import 'package:appflowy/mobile/application/base/mobile_view_page_bloc.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
import 'package:appflowy/mobile/presentation/widgets/flowy_mobile_quick_action_button.dart';
import 'package:appflowy/plugins/shared/share/share_bloc.dart';
import 'package:appflowy/workspace/application/view/view_ext.dart';
import 'package:appflowy/workspace/application/view/view_lock_status_bloc.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@@ -28,27 +25,11 @@ enum MobileViewBottomSheetBodyAction {
visitSite,
copyShareLink,
updatePathName,
lockPage;
static const disableInLockedView = [
undo,
redo,
rename,
delete,
];
}
class MobileViewBottomSheetBodyActionArguments {
static const isLockedKey = 'is_locked';
}
typedef MobileViewBottomSheetBodyActionCallback = void Function(
MobileViewBottomSheetBodyAction action,
// for the [MobileViewBottomSheetBodyAction.lockPage] action,
// it will pass the [isLocked] value to the callback.
{
Map<String, dynamic>? arguments,
});
);
class ViewPageBottomSheet extends StatefulWidget {
const ViewPageBottomSheet({
@@ -75,7 +56,7 @@ class _ViewPageBottomSheetState extends State<ViewPageBottomSheet> {
case MobileBottomSheetType.view:
return MobileViewBottomSheetBody(
view: widget.view,
onAction: (action, {arguments}) {
onAction: (action) {
switch (action) {
case MobileViewBottomSheetBodyAction.rename:
setState(() {
@@ -83,7 +64,7 @@ class _ViewPageBottomSheetState extends State<ViewPageBottomSheet> {
});
break;
default:
widget.onAction(action, arguments: arguments);
widget.onAction(action);
}
},
);
@@ -112,8 +93,6 @@ class MobileViewBottomSheetBody extends StatelessWidget {
@override
Widget build(BuildContext context) {
final isFavorite = view.isFavorite;
final isLocked =
context.watch<ViewLockStatusBloc?>()?.state.isLocked ?? false;
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
@@ -121,7 +100,6 @@ class MobileViewBottomSheetBody extends StatelessWidget {
text: LocaleKeys.button_rename.tr(),
icon: FlowySvgs.view_item_rename_s,
iconSize: const Size.square(18),
enable: !isLocked,
onTap: () => onAction(
MobileViewBottomSheetBodyAction.rename,
),
@@ -140,28 +118,6 @@ class MobileViewBottomSheetBody extends StatelessWidget {
),
),
_divider(),
if (view.layout.isDatabaseView || view.layout.isDocumentView) ...[
MobileQuickActionButton(
text: LocaleKeys.disclosureAction_lockPage.tr(),
icon: FlowySvgs.lock_page_s,
iconSize: const Size.square(18),
rightIconBuilder: (context) => _LockPageRightIconBuilder(
onAction: onAction,
),
onTap: () {
final isLocked =
context.read<ViewLockStatusBloc?>()?.state.isLocked ?? false;
onAction(
MobileViewBottomSheetBodyAction.lockPage,
arguments: {
MobileViewBottomSheetBodyActionArguments.isLockedKey:
!isLocked,
},
);
},
),
_divider(),
],
MobileQuickActionButton(
text: LocaleKeys.button_duplicate.tr(),
icon: FlowySvgs.duplicate_s,
@@ -182,14 +138,12 @@ class MobileViewBottomSheetBody extends StatelessWidget {
),
_divider(),
..._buildPublishActions(context),
_divider(),
MobileQuickActionButton(
text: LocaleKeys.button_delete.tr(),
textColor: Theme.of(context).colorScheme.error,
icon: FlowySvgs.trash_s,
iconColor: Theme.of(context).colorScheme.error,
iconSize: const Size.square(18),
enable: !isLocked,
onTap: () => onAction(
MobileViewBottomSheetBodyAction.delete,
),
@@ -252,36 +206,3 @@ class MobileViewBottomSheetBody extends StatelessWidget {
Widget _divider() => const MobileQuickActionDivider();
}
class _LockPageRightIconBuilder extends StatelessWidget {
const _LockPageRightIconBuilder({
required this.onAction,
});
final MobileViewBottomSheetBodyActionCallback onAction;
@override
Widget build(BuildContext context) {
final isLocked =
context.watch<ViewLockStatusBloc?>()?.state.isLocked ?? false;
return SizedBox(
width: 46,
height: 30,
child: FittedBox(
fit: BoxFit.fill,
child: CupertinoSwitch(
value: isLocked,
activeTrackColor: Theme.of(context).colorScheme.primary,
onChanged: (value) {
onAction(
MobileViewBottomSheetBodyAction.lockPage,
arguments: {
MobileViewBottomSheetBodyActionArguments.isLockedKey: value,
},
);
},
),
),
);
}
}
@@ -8,7 +8,6 @@ import 'package:appflowy/workspace/application/recent/recent_views_bloc.dart';
import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart';
import 'package:appflowy/workspace/application/view/view_bloc.dart';
import 'package:appflowy/workspace/application/view/view_ext.dart';
import 'package:appflowy/workspace/application/view/view_lock_status_bloc.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:easy_localization/easy_localization.dart';
@@ -132,11 +131,6 @@ enum MobilePaneActionType {
BlocProvider.value(value: favoriteBloc),
if (recentViewsBloc != null)
BlocProvider.value(value: recentViewsBloc),
BlocProvider(
create: (_) =>
ViewLockStatusBloc(view: viewBloc.state.view)
..add(const ViewLockStatusEvent.initial()),
),
],
child: BlocBuilder<ViewBloc, ViewState>(
builder: (context, state) {
@@ -74,7 +74,7 @@ Future<T?> showMobileBottomSheet<T>(
backgroundColor ??= Theme.of(context).brightness == Brightness.light
? const Color(0xFFF7F8FB)
: const Color(0xFF23262B);
barrierColor ??= Colors.black.withValues(alpha: 0.3);
barrierColor ??= Colors.black.withOpacity(0.3);
return showModalBottomSheet<T>(
context: context,
@@ -329,7 +329,7 @@ class CupertinoSheetBottomRouteTransition extends StatelessWidget {
(Theme.of(context).brightness == Brightness.dark
? Colors.grey
: Colors.black)
.withValues(alpha: secondaryAnimation.value * 0.1),
.withOpacity(secondaryAnimation.value * 0.1),
BlendMode.srcOver,
),
child: child,
@@ -1,3 +1,5 @@
import 'package:flutter/material.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/database/board/board.dart';
@@ -11,14 +13,12 @@ import 'package:appflowy/plugins/database/widgets/cell/card_cell_style_maps/mobi
import 'package:appflowy/shared/flowy_error_page.dart';
import 'package:appflowy/util/field_type_extension.dart';
import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
import 'package:appflowy/workspace/application/view/view_lock_status_bloc.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
import 'package:appflowy_board/appflowy_board.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
@@ -143,8 +143,6 @@ class _BoardContentState extends State<_BoardContent> {
return state.maybeMap(
orElse: () => const SizedBox.shrink(),
ready: (state) {
final isLocked =
context.watch<ViewLockStatusBloc?>()?.state.isLocked ?? false;
final showCreateGroupButton = context
.read<BoardBloc>()
.groupingFieldType
@@ -162,20 +160,15 @@ class _BoardContentState extends State<_BoardContent> {
padding: config.groupHeaderPadding,
)
: const HSpace(16),
trailing: showCreateGroupButton && !isLocked
trailing: showCreateGroupButton
? const MobileBoardTrailing()
: const HSpace(16),
headerBuilder: (_, groupData) {
final isLocked =
context.read<ViewLockStatusBloc?>()?.state.isLocked ??
false;
return IgnorePointer(
ignoring: isLocked,
child: GroupCardHeader(
groupData: groupData,
),
);
},
headerBuilder: (_, groupData) => BlocProvider<BoardBloc>.value(
value: context.read<BoardBloc>(),
child: GroupCardHeader(
groupData: groupData,
),
),
footerBuilder: _buildFooter,
cardBuilder: (_, column, columnItem) => _buildCard(
context: context,
@@ -191,39 +184,34 @@ class _BoardContentState extends State<_BoardContent> {
}
Widget _buildFooter(BuildContext context, AppFlowyGroupData columnData) {
final isLocked =
context.read<ViewLockStatusBloc?>()?.state.isLocked ?? false;
final style = Theme.of(context);
return SizedBox(
height: 42,
width: double.infinity,
child: IgnorePointer(
ignoring: isLocked,
child: TextButton.icon(
style: TextButton.styleFrom(
padding: const EdgeInsets.only(left: 8),
alignment: Alignment.centerLeft,
),
icon: FlowySvg(
FlowySvgs.add_m,
child: TextButton.icon(
style: TextButton.styleFrom(
padding: const EdgeInsets.only(left: 8),
alignment: Alignment.centerLeft,
),
icon: FlowySvg(
FlowySvgs.add_m,
color: style.colorScheme.onSurface,
),
label: Text(
LocaleKeys.board_column_createNewCard.tr(),
style: style.textTheme.bodyMedium?.copyWith(
color: style.colorScheme.onSurface,
),
label: Text(
LocaleKeys.board_column_createNewCard.tr(),
style: style.textTheme.bodyMedium?.copyWith(
color: style.colorScheme.onSurface,
),
),
onPressed: () => context.read<BoardBloc>().add(
BoardEvent.createRow(
columnData.id,
OrderObjectPositionTypePB.End,
null,
null,
),
),
),
onPressed: () => context.read<BoardBloc>().add(
BoardEvent.createRow(
columnData.id,
OrderObjectPositionTypePB.End,
null,
null,
),
),
),
);
}
@@ -243,8 +231,6 @@ class _BoardContentState extends State<_BoardContent> {
CardCellBuilder(databaseController: boardBloc.databaseController);
final groupItemId = groupItem.row.id + groupData.group.groupId;
final isLocked =
context.read<ViewLockStatusBloc?>()?.state.isLocked ?? false;
return Container(
key: ValueKey(groupItemId),
@@ -252,34 +238,31 @@ class _BoardContentState extends State<_BoardContent> {
decoration: _makeBoxDecoration(context),
child: BlocProvider.value(
value: boardBloc,
child: IgnorePointer(
ignoring: isLocked,
child: RowCard(
fieldController: boardBloc.fieldController,
rowMeta: rowMeta,
viewId: boardBloc.viewId,
rowCache: boardBloc.rowCache,
groupingFieldId: groupItem.fieldInfo.id,
isEditing: false,
cellBuilder: cellBuilder,
onTap: (context) {
context.push(
MobileRowDetailPage.routeName,
extra: {
MobileRowDetailPage.argRowId: rowMeta.id,
MobileRowDetailPage.argDatabaseController:
context.read<BoardBloc>().databaseController,
},
);
},
onStartEditing: () {},
onEndEditing: () {},
styleConfiguration: RowCardStyleConfiguration(
cellStyleMap: mobileBoardCardCellStyleMap(context),
showAccessory: false,
),
userProfile: boardBloc.userProfile,
child: RowCard(
fieldController: boardBloc.fieldController,
rowMeta: rowMeta,
viewId: boardBloc.viewId,
rowCache: boardBloc.rowCache,
groupingFieldId: groupItem.fieldInfo.id,
isEditing: false,
cellBuilder: cellBuilder,
onTap: (context) {
context.push(
MobileRowDetailPage.routeName,
extra: {
MobileRowDetailPage.argRowId: rowMeta.id,
MobileRowDetailPage.argDatabaseController:
context.read<BoardBloc>().databaseController,
},
);
},
onStartEditing: () {},
onEndEditing: () {},
styleConfiguration: RowCardStyleConfiguration(
cellStyleMap: mobileBoardCardCellStyleMap(context),
showAccessory: false,
),
userProfile: boardBloc.userProfile,
),
),
);
@@ -293,20 +276,14 @@ class _BoardContentState extends State<_BoardContent> {
border: themeMode == ThemeMode.light
? Border.fromBorderSide(
BorderSide(
color: Theme.of(context)
.colorScheme
.outline
.withValues(alpha: 0.5),
color: Theme.of(context).colorScheme.outline.withOpacity(0.5),
),
)
: null,
boxShadow: themeMode == ThemeMode.light
? [
BoxShadow(
color: Theme.of(context)
.colorScheme
.outline
.withValues(alpha: 0.5),
color: Theme.of(context).colorScheme.outline.withOpacity(0.5),
blurRadius: 4,
offset: const Offset(0, 2),
),
@@ -1,3 +1,5 @@
import 'package:flutter/material.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/base/app_bar/app_bar.dart';
@@ -27,7 +29,6 @@ import 'package:appflowy_backend/protobuf/flowy-database2/row_entities.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:go_router/go_router.dart';
@@ -58,9 +59,7 @@ class _MobileRowDetailPageState extends State<MobileRowDetailPage> {
late final PageController _pageController;
String get viewId => widget.databaseController.viewId;
RowCache get rowCache => widget.databaseController.rowCache;
FieldController get fieldController =>
widget.databaseController.fieldController;
@@ -381,9 +380,7 @@ class MobileRowDetailPageContentState
late final EditableCellBuilder cellBuilder;
String get viewId => widget.databaseController.viewId;
RowCache get rowCache => widget.databaseController.rowCache;
FieldController get fieldController =>
widget.databaseController.fieldController;
ValueNotifier<String> primaryFieldId = ValueNotifier('');
@@ -545,7 +542,6 @@ class _TitleSkin extends IEditableTextCellSkin {
Widget build(
BuildContext context,
CellContainerNotifier cellContainerNotifier,
ValueNotifier<bool> compactModeNotifier,
TextCellBloc bloc,
FocusNode focusNode,
TextEditingController textEditingController,
@@ -1,3 +1,5 @@
import 'package:flutter/material.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/base/app_bar/app_bar.dart';
import 'package:appflowy/mobile/presentation/database/field/mobile_full_field_editor.dart';
@@ -6,7 +8,6 @@ import 'package:appflowy/plugins/database/domain/field_backend_service.dart';
import 'package:appflowy/plugins/database/domain/field_service.dart';
import 'package:appflowy/plugins/database/widgets/setting/field_visibility_extension.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
class MobileEditPropertyScreen extends StatefulWidget {
@@ -48,7 +49,7 @@ class _MobileEditPropertyScreenState extends State<MobileEditPropertyScreen> {
final fieldId = widget.field.id;
return PopScope(
onPopInvokedWithResult: (didPop, _) {
onPopInvoked: (didPop) {
if (!didPop) {
context.pop(_fieldOptionValues);
}
@@ -75,7 +75,7 @@ class MobileDatabaseViewQuickActions extends StatelessWidget {
enableBackgroundColorSelection: false,
onSelectedEmoji: (r) {
ViewBackendService.updateViewIcon(
view: view,
viewId: view.id,
viewIcon: r.data,
);
Navigator.pop(context);
@@ -84,11 +84,7 @@ class MobileDatabaseViewQuickActions extends StatelessWidget {
);
},
builder: (_) => const SizedBox.shrink(),
).then((_) {
if (context.mounted) {
Navigator.pop(context);
}
});
).then((_) => Navigator.pop(context));
},
!isInline,
),
@@ -3,19 +3,16 @@ import 'package:appflowy/env/env.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/base/app_bar/app_bar.dart';
import 'package:appflowy/mobile/presentation/presentation.dart';
import 'package:appflowy/mobile/presentation/setting/ai/ai_settings_group.dart';
import 'package:appflowy/mobile/presentation/setting/cloud/cloud_setting_group.dart';
import 'package:appflowy/mobile/presentation/setting/user_session_setting_group.dart';
import 'package:appflowy/mobile/presentation/setting/workspace/workspace_setting_group.dart';
import 'package:appflowy/mobile/presentation/widgets/widgets.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/application/auth/auth_service.dart';
import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class MobileHomeSettingPage extends StatefulWidget {
const MobileHomeSettingPage({
@@ -73,47 +70,29 @@ class _MobileHomeSettingPageState extends State<MobileHomeSettingPage> {
Widget _buildSettingsWidget(UserProfilePB userProfile) {
// show the third-party sign in buttons if user logged in with local session and auth is enabled.
final isLocalAuthEnabled =
final showThirdPartyLogin =
userProfile.authenticator == AuthenticatorPB.Local && isAuthEnabled;
'';
return BlocProvider(
create: (context) => UserWorkspaceBloc(userProfile: userProfile)
..add(const UserWorkspaceEvent.initial()),
child: BlocBuilder<UserWorkspaceBloc, UserWorkspaceState>(
builder: (context, state) {
final currentWorkspaceId = state.currentWorkspace?.workspaceId ?? '';
return SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
PersonalInfoSettingGroup(
userProfile: userProfile,
),
const WorkspaceSettingGroup(),
const AppearanceSettingGroup(),
const LanguageSettingGroup(),
if (Env.enableCustomCloud) const CloudSettingGroup(),
if (isAuthEnabled)
AiSettingsGroup(
key: ValueKey(currentWorkspaceId),
userProfile: userProfile,
workspaceId: currentWorkspaceId,
currentWorkspaceMemberRole: state.currentWorkspace?.role,
),
const SupportSettingGroup(),
const AboutSettingGroup(),
UserSessionSettingGroup(
userProfile: userProfile,
showThirdPartyLogin: isLocalAuthEnabled,
),
const VSpace(20),
],
),
return SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
PersonalInfoSettingGroup(
userProfile: userProfile,
),
);
},
const WorkspaceSettingGroup(),
const AppearanceSettingGroup(),
const LanguageSettingGroup(),
if (Env.enableCustomCloud) const CloudSettingGroup(),
const SupportSettingGroup(),
const AboutSettingGroup(),
UserSessionSettingGroup(
userProfile: userProfile,
showThirdPartyLogin: showThirdPartyLogin,
),
const VSpace(20),
],
),
),
);
}
@@ -212,7 +212,7 @@ class _DeletedFilesListView extends StatelessWidget {
?.copyWith(color: theme.colorScheme.onSurface),
),
horizontalTitleGap: 0,
tileColor: theme.colorScheme.onSurface.withValues(alpha: 0.1),
tileColor: theme.colorScheme.onSurface.withOpacity(0.1),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
@@ -134,8 +134,7 @@ class _RecentCover extends StatelessWidget {
Widget build(BuildContext context) {
final placeholder = Container(
// random color, update it once we have a better placeholder
color:
Theme.of(context).colorScheme.onSurfaceVariant.withValues(alpha: 0.2),
color: Theme.of(context).colorScheme.onSurfaceVariant.withOpacity(0.2),
);
final value = this.value;
if (value == null) {
@@ -225,7 +225,7 @@ class MobileViewPage extends StatelessWidget {
Widget _buildLastViewed(BuildContext context) {
final textColor = Theme.of(context).isLightMode
? const Color(0x7F171717)
: Colors.white.withValues(alpha: 0.45);
: Colors.white.withOpacity(0.45);
if (timestamp == null) {
return const SizedBox.shrink();
}
@@ -98,7 +98,7 @@ class MobileSpace extends StatelessWidget {
Navigator.of(sheetContext).pop();
context.read<SpaceBloc>().add(
SpaceEvent.createPage(
name: '',
name: layout.defaultName,
layout: layout,
index: 0,
openAfterCreate: true,
@@ -42,7 +42,7 @@ class FloatingAIEntry extends StatelessWidget {
blurRadius: 20,
spreadRadius: 1,
offset: const Offset(0, 4),
color: Colors.black.withValues(alpha: 0.05),
color: Colors.black.withOpacity(0.05),
),
],
);
@@ -51,8 +51,8 @@ class FloatingAIEntry extends StatelessWidget {
BoxDecoration _buildWrapperDecoration(BuildContext context) {
final outlineColor = Theme.of(context).colorScheme.outline;
final borderColor = Theme.of(context).isLightMode
? outlineColor.withValues(alpha: 0.7)
: outlineColor.withValues(alpha: 0.3);
? outlineColor.withOpacity(0.7)
: outlineColor.withOpacity(0.3);
return BoxDecoration(
borderRadius: BorderRadius.circular(30),
color: Theme.of(context).colorScheme.surface,
@@ -11,6 +11,7 @@ import 'package:appflowy/workspace/application/menu/sidebar_sections_bloc.dart';
import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart';
import 'package:appflowy/workspace/application/sidebar/space/space_bloc.dart';
import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart';
import 'package:appflowy/workspace/application/view/view_ext.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
@@ -72,14 +73,7 @@ class _MobileSpaceTabState extends State<MobileSpaceTab>
listener: (context, state) {
final lastCreatedPage = state.lastCreatedPage;
if (lastCreatedPage != null) {
context.pushView(
lastCreatedPage,
tabs: [
PickerTabType.emoji,
PickerTabType.icon,
PickerTabType.custom,
].map((e) => e.name).toList(),
);
context.pushView(lastCreatedPage);
}
},
),
@@ -179,6 +173,8 @@ class _MobileSpaceTabState extends State<MobileSpaceTab>
);
case MobileSpaceTabType.favorites:
return MobileFavoriteSpace(userProfile: widget.userProfile);
default:
throw Exception('Unknown tab type: $tab');
}
}).toList();
}
@@ -192,7 +188,7 @@ class _MobileSpaceTabState extends State<MobileSpaceTab>
if (context.read<SpaceBloc>().state.spaces.isNotEmpty) {
context.read<SpaceBloc>().add(
SpaceEvent.createPage(
name: '',
name: layout.defaultName,
layout: layout,
openAfterCreate: true,
),
@@ -201,7 +197,7 @@ class _MobileSpaceTabState extends State<MobileSpaceTab>
// only support create document in section
context.read<SidebarSectionsBloc>().add(
SidebarSectionsEvent.createRootViewInSection(
name: '',
name: layout.defaultName,
index: 0,
viewSection: FolderSpaceType.public.toViewSectionPB,
),
@@ -139,7 +139,7 @@ class _CreateWorkspaceButton extends StatelessWidget {
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: const Color(0x01717171).withValues(alpha: 0.12),
color: const Color(0x01717171).withOpacity(0.12),
width: 0.8,
),
),
@@ -101,6 +101,8 @@ class WorkspaceMenuMoreOptions extends StatelessWidget {
WorkspaceMenuMoreOption.leave,
),
);
default:
return const Placeholder();
}
}
}
@@ -1,253 +0,0 @@
import 'dart:async';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/inline_actions/inline_actions_menu.dart';
import 'package:appflowy/plugins/inline_actions/inline_actions_result.dart';
import 'package:appflowy/plugins/inline_actions/inline_actions_service.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:collection/collection.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flutter/material.dart';
import 'mobile_inline_actions_menu_group.dart';
extension _StartWithsSort on List<InlineActionsResult> {
void sortByStartsWithKeyword(String search) => sort(
(a, b) {
final aCount = a.startsWithKeywords
?.where(
(key) => search.toLowerCase().startsWith(key),
)
.length ??
0;
final bCount = b.startsWithKeywords
?.where(
(key) => search.toLowerCase().startsWith(key),
)
.length ??
0;
if (aCount > bCount) {
return -1;
} else if (bCount > aCount) {
return 1;
}
return 0;
},
);
}
const _invalidSearchesAmount = 10;
class MobileInlineActionsHandler extends StatefulWidget {
const MobileInlineActionsHandler({
super.key,
required this.results,
required this.editorState,
required this.menuService,
required this.onDismiss,
required this.style,
required this.service,
this.startCharAmount = 1,
this.startOffset = 0,
this.cancelBySpaceHandler,
});
final List<InlineActionsResult> results;
final EditorState editorState;
final InlineActionsMenuService menuService;
final VoidCallback onDismiss;
final InlineActionsMenuStyle style;
final int startCharAmount;
final InlineActionsService service;
final bool Function()? cancelBySpaceHandler;
final int startOffset;
@override
State<MobileInlineActionsHandler> createState() =>
_MobileInlineActionsHandlerState();
}
class _MobileInlineActionsHandlerState
extends State<MobileInlineActionsHandler> {
final _focusNode =
FocusNode(debugLabel: 'mobile_inline_actions_menu_handler');
late List<InlineActionsResult> results = widget.results;
int invalidCounter = 0;
late int startOffset;
String _search = '';
set search(String search) {
_search = search;
_doSearch();
}
Future<void> _doSearch() async {
final List<InlineActionsResult> newResults = [];
for (final handler in widget.service.handlers) {
final group = await handler.search(_search);
if (group.results.isNotEmpty) {
newResults.add(group);
}
}
invalidCounter = results.every((group) => group.results.isEmpty)
? invalidCounter + 1
: 0;
if (invalidCounter >= _invalidSearchesAmount) {
widget.onDismiss();
// Workaround to bring focus back to editor
await editorState.updateSelectionWithReason(editorState.selection);
return;
}
_resetSelection();
newResults.sortByStartsWithKeyword(_search);
setState(() => results = newResults);
}
void _resetSelection() {
_selectedGroup = 0;
_selectedIndex = 0;
}
int _selectedGroup = 0;
int _selectedIndex = 0;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback(
(_) => _focusNode.requestFocus(),
);
startOffset = editorState.selection?.endIndex ?? 0;
keepEditorFocusNotifier.increase();
editorState.selectionNotifier.addListener(onSelectionChanged);
}
@override
void dispose() {
editorState.selectionNotifier.removeListener(onSelectionChanged);
_focusNode.dispose();
keepEditorFocusNotifier.decrease();
super.dispose();
}
@override
Widget build(BuildContext context) {
final width = editorState.renderBox!.size.width - 24 * 2;
return Focus(
focusNode: _focusNode,
child: Container(
constraints: BoxConstraints(
maxHeight: 192,
minWidth: width,
maxWidth: width,
),
margin: EdgeInsets.symmetric(horizontal: 24.0),
decoration: BoxDecoration(
color: widget.style.backgroundColor,
borderRadius: BorderRadius.circular(6.0),
boxShadow: [
BoxShadow(
blurRadius: 5,
spreadRadius: 1,
color: Colors.black.withValues(alpha: 0.1),
),
],
),
child: noResults
? SizedBox(
width: 150,
child: FlowyText.regular(
LocaleKeys.inlineActions_noResults.tr(),
),
)
: SingleChildScrollView(
physics: const ClampingScrollPhysics(),
child: Material(
color: Colors.transparent,
child: Padding(
padding: EdgeInsets.all(6.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: results
.where((g) => g.results.isNotEmpty)
.mapIndexed(
(index, group) => MobileInlineActionsGroup(
result: group,
editorState: editorState,
menuService: menuService,
style: widget.style,
onSelected: widget.onDismiss,
startOffset: startOffset - widget.startCharAmount,
endOffset:
_search.length + widget.startCharAmount,
isLastGroup: index == results.length - 1,
isGroupSelected: _selectedGroup == index,
selectedIndex: _selectedIndex,
onPreSelect: (int value) {
setState(() {
_selectedGroup = index;
_selectedIndex = value;
});
},
),
)
.toList(),
),
),
),
),
),
);
}
bool get noResults =>
results.isEmpty || results.every((e) => e.results.isEmpty);
int get groupLength => results.length;
int lengthOfGroup(int index) =>
results.length > index ? results[index].results.length : -1;
InlineActionsMenuItem handlerOf(int groupIndex, int handlerIndex) =>
results[groupIndex].results[handlerIndex];
EditorState get editorState => widget.editorState;
InlineActionsMenuService get menuService => widget.menuService;
void onSelectionChanged() {
final selection = editorState.selection;
if (selection == null) {
menuService.dismiss();
return;
}
if (!selection.isCollapsed) {
menuService.dismiss();
return;
}
final startOffset = widget.startOffset;
final endOffset = selection.end.offset;
if (endOffset < startOffset) {
menuService.dismiss();
return;
}
final node = editorState.getNodeAtPath(selection.start.path);
final text = node?.delta?.toPlainText() ?? '';
final search = text.substring(startOffset, endOffset);
this.search = search;
}
}
@@ -1,151 +0,0 @@
import 'dart:async';
import 'package:appflowy/plugins/inline_actions/inline_actions_menu.dart';
import 'package:appflowy/plugins/inline_actions/inline_actions_result.dart';
import 'package:appflowy/plugins/inline_actions/inline_actions_service.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/material.dart';
import 'mobile_inline_actions_handler.dart';
class MobileInlineActionsMenu extends InlineActionsMenuService {
MobileInlineActionsMenu({
required this.context,
required this.editorState,
required this.initialResults,
required this.style,
required this.service,
this.startCharAmount = 1,
this.cancelBySpaceHandler,
});
final BuildContext context;
final EditorState editorState;
final List<InlineActionsResult> initialResults;
final bool Function()? cancelBySpaceHandler;
final InlineActionsService service;
@override
final InlineActionsMenuStyle style;
final int startCharAmount;
OverlayEntry? _menuEntry;
@override
void dismiss() {
if (_menuEntry != null) {
editorState.service.keyboardService?.enable();
editorState.service.scrollService?.enable();
}
_menuEntry?.remove();
_menuEntry = null;
}
@override
Future<void> show() {
final completer = Completer<void>();
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
_show();
completer.complete();
});
return completer.future;
}
void _show() {
final selectionRects = editorState.selectionRects();
if (selectionRects.isEmpty) {
return;
}
const double menuHeight = 192.0;
const Offset menuOffset = Offset(0, 10);
final Offset editorOffset =
editorState.renderBox?.localToGlobal(Offset.zero) ?? Offset.zero;
final Size editorSize = editorState.renderBox!.size;
// Default to opening the overlay below
Alignment alignment = Alignment.topLeft;
final firstRect = selectionRects.first;
Offset offset = firstRect.bottomRight + menuOffset;
// Show above
if (offset.dy + menuHeight >= editorOffset.dy + editorSize.height) {
offset = firstRect.topRight - menuOffset;
alignment = Alignment.bottomLeft;
offset = Offset(
offset.dx,
MediaQuery.of(context).size.height - offset.dy,
);
}
final (left, top, right, bottom) = _getPosition(alignment, offset);
_menuEntry = OverlayEntry(
builder: (context) => SizedBox(
width: editorSize.width,
height: editorSize.height,
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: dismiss,
child: Stack(
children: [
Positioned(
top: top,
bottom: bottom,
left: left,
right: right,
child: MobileInlineActionsHandler(
service: service,
results: initialResults,
editorState: editorState,
menuService: this,
onDismiss: dismiss,
style: style,
startCharAmount: startCharAmount,
cancelBySpaceHandler: cancelBySpaceHandler,
startOffset: editorState.selection?.start.offset ?? 0,
),
),
],
),
),
),
);
Overlay.of(context).insert(_menuEntry!);
editorState.service.keyboardService?.disable(showCursor: true);
editorState.service.scrollService?.disable();
}
(double? left, double? top, double? right, double? bottom) _getPosition(
Alignment alignment,
Offset offset,
) {
double? left, top, right, bottom;
switch (alignment) {
case Alignment.topLeft:
left = 0;
top = offset.dy;
break;
case Alignment.bottomLeft:
left = 0;
bottom = offset.dy;
break;
case Alignment.topRight:
right = offset.dx;
top = offset.dy;
break;
case Alignment.bottomRight:
right = offset.dx;
bottom = offset.dy;
break;
}
return (left, top, right, bottom);
}
}
@@ -1,152 +0,0 @@
import 'package:appflowy/plugins/inline_actions/inline_actions_menu.dart';
import 'package:appflowy/plugins/inline_actions/inline_actions_result.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:collection/collection.dart';
import 'package:flowy_infra_ui/style_widget/button.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flutter/material.dart';
class MobileInlineActionsGroup extends StatelessWidget {
const MobileInlineActionsGroup({
super.key,
required this.result,
required this.editorState,
required this.menuService,
required this.style,
required this.onSelected,
required this.startOffset,
required this.endOffset,
required this.onPreSelect,
this.isLastGroup = false,
this.isGroupSelected = false,
this.selectedIndex = 0,
});
final InlineActionsResult result;
final EditorState editorState;
final InlineActionsMenuService menuService;
final InlineActionsMenuStyle style;
final VoidCallback onSelected;
final ValueChanged<int> onPreSelect;
final int startOffset;
final int endOffset;
final bool isLastGroup;
final bool isGroupSelected;
final int selectedIndex;
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (result.title != null) ...[
SizedBox(
height: 36,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: Align(
alignment: Alignment.centerLeft,
child: FlowyText.medium(
result.title!,
color: style.groupTextColor,
fontSize: 12,
),
),
),
),
],
...result.results.mapIndexed(
(index, item) => GestureDetector(
onTapDown: (e) {
onPreSelect.call(index);
},
child: MobileInlineActionsWidget(
item: item,
editorState: editorState,
menuService: menuService,
isSelected: isGroupSelected && index == selectedIndex,
style: style,
onSelected: onSelected,
startOffset: startOffset,
endOffset: endOffset,
),
),
),
],
);
}
}
class MobileInlineActionsWidget extends StatelessWidget {
const MobileInlineActionsWidget({
super.key,
required this.item,
required this.editorState,
required this.menuService,
required this.isSelected,
required this.style,
required this.onSelected,
required this.startOffset,
required this.endOffset,
});
final InlineActionsMenuItem item;
final EditorState editorState;
final InlineActionsMenuService menuService;
final bool isSelected;
final InlineActionsMenuStyle style;
final VoidCallback onSelected;
final int startOffset;
final int endOffset;
@override
Widget build(BuildContext context) {
final hasIcon = item.icon != null;
return Container(
height: 36,
decoration: BoxDecoration(
color: isSelected ? style.menuItemSelectedColor : null,
borderRadius: BorderRadius.circular(6.0),
),
child: FlowyButton(
expand: true,
isSelected: isSelected,
text: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: Align(
alignment: Alignment.centerLeft,
child: Row(
children: [
if (hasIcon) ...[
item.icon!.call(isSelected),
SizedBox(width: 12),
],
Flexible(
child: FlowyText.regular(
item.label,
figmaLineHeight: 18,
overflow: TextOverflow.ellipsis,
fontSize: 16,
color: style.menuItemSelectedTextColor,
),
),
],
),
),
),
onTap: () => _onPressed(context),
),
);
}
void _onPressed(BuildContext context) {
onSelected();
item.onSelected?.call(
context,
editorState,
menuService,
(startOffset, endOffset),
);
}
}
@@ -365,14 +365,14 @@ class _NotificationNavigationBar extends StatelessWidget {
extension on BuildContext {
Color get backgroundColor {
return Theme.of(this).isLightMode
? Colors.white.withValues(alpha: 0.95)
: const Color(0xFF23262B).withValues(alpha: 0.95);
? Colors.white.withOpacity(0.95)
: const Color(0xFF23262B).withOpacity(0.95);
}
Color get borderColor {
return Theme.of(this).isLightMode
? const Color(0x141F2329)
: const Color(0xFF23262B).withValues(alpha: 0.5);
: const Color(0xFF23262B).withOpacity(0.5);
}
Border? get border {
@@ -6,6 +6,6 @@ extension NotificationItemColors on BuildContext {
if (Theme.of(this).isLightMode) {
return const Color(0xFF171717);
}
return const Color(0xFFffffff).withValues(alpha: 0.8);
return const Color(0xFFffffff).withOpacity(0.8);
}
}
@@ -1,220 +0,0 @@
import 'dart:async';
import 'dart:math';
import 'package:appflowy/mobile/presentation/selection_menu/mobile_selection_menu_item.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/material.dart';
import 'mobile_selection_menu_item_widget.dart';
import 'mobile_selection_menu_widget.dart';
class MobileSelectionMenu extends SelectionMenuService {
MobileSelectionMenu({
required this.context,
required this.editorState,
required this.selectionMenuItems,
this.deleteSlashByDefault = false,
this.deleteKeywordsByDefault = false,
this.style = MobileSelectionMenuStyle.light,
this.itemCountFilter = 0,
this.startOffset = 0,
this.singleColumn = false,
});
final BuildContext context;
final EditorState editorState;
final List<SelectionMenuItem> selectionMenuItems;
final bool deleteSlashByDefault;
final bool deleteKeywordsByDefault;
final bool singleColumn;
@override
final MobileSelectionMenuStyle style;
OverlayEntry? _selectionMenuEntry;
Offset _offset = Offset.zero;
Alignment _alignment = Alignment.topLeft;
final int itemCountFilter;
final int startOffset;
@override
void dismiss() {
if (_selectionMenuEntry != null) {
editorState.service.keyboardService?.enable();
editorState.service.scrollService?.enable();
}
_selectionMenuEntry?.remove();
_selectionMenuEntry = null;
}
@override
Future<void> show() async {
final completer = Completer<void>();
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
_show();
completer.complete();
});
return completer.future;
}
void _show() {
final selectionRects = editorState.selectionRects();
if (selectionRects.isEmpty) {
return;
}
calculateSelectionMenuOffset(selectionRects.first);
final (left, top, right, bottom) = getPosition();
final editorHeight = editorState.renderBox!.size.height;
final editorWidth = editorState.renderBox!.size.width;
_selectionMenuEntry = OverlayEntry(
builder: (context) {
return SizedBox(
width: editorWidth,
height: editorHeight,
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: dismiss,
child: Stack(
children: [
Positioned(
top: top,
bottom: bottom,
left: left,
right: right,
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: MobileSelectionMenuWidget(
selectionMenuStyle: style,
singleColumn: singleColumn,
items: selectionMenuItems
..forEach((element) {
if (element is MobileSelectionMenuItem) {
element.deleteSlash = false;
element.deleteKeywords = deleteKeywordsByDefault;
for (final e in element.children) {
e.deleteSlash = deleteSlashByDefault;
e.deleteKeywords = deleteKeywordsByDefault;
e.onSelected = () {
dismiss();
};
}
} else {
element.deleteSlash = deleteSlashByDefault;
element.deleteKeywords = deleteKeywordsByDefault;
element.onSelected = () {
dismiss();
};
}
}),
maxItemInRow: 5,
editorState: editorState,
itemCountFilter: itemCountFilter,
startOffset: startOffset,
menuService: this,
onExit: () {
dismiss();
},
deleteSlashByDefault: deleteSlashByDefault,
),
),
),
],
),
),
);
},
);
Overlay.of(context, rootOverlay: true).insert(_selectionMenuEntry!);
editorState.service.keyboardService?.disable(showCursor: true);
editorState.service.scrollService?.disable();
}
@override
Alignment get alignment {
return _alignment;
}
@override
Offset get offset {
return _offset;
}
@override
(double? left, double? top, double? right, double? bottom) getPosition() {
double? left, top, right, bottom;
switch (alignment) {
case Alignment.topLeft:
left = offset.dx;
top = offset.dy;
break;
case Alignment.bottomLeft:
left = offset.dx;
bottom = offset.dy;
break;
case Alignment.topRight:
right = offset.dx;
top = offset.dy;
break;
case Alignment.bottomRight:
right = offset.dx;
bottom = offset.dy;
break;
}
return (left, top, right, bottom);
}
void calculateSelectionMenuOffset(Rect rect) {
// Workaround: We can customize the padding through the [EditorStyle],
// but the coordinates of overlay are not properly converted currently.
// Just subtract the padding here as a result.
const menuHeight = 192.0, menuWidth = 240.0 + 10;
const menuOffset = Offset(0, 10);
final editorOffset =
editorState.renderBox?.localToGlobal(Offset.zero) ?? Offset.zero;
final editorHeight = editorState.renderBox!.size.height;
final editorWidth = editorState.renderBox!.size.width;
// show below default
_alignment = Alignment.topLeft;
final bottomRight = rect.bottomRight;
final topRight = rect.topRight;
var offset = bottomRight + menuOffset;
_offset = Offset(
offset.dx,
offset.dy,
);
// show above
if (offset.dy + menuHeight >= editorOffset.dy + editorHeight) {
offset = topRight - menuOffset;
_alignment = Alignment.bottomLeft;
final limitX = editorWidth - menuWidth;
_offset = Offset(
min(offset.dx, limitX),
MediaQuery.of(context).size.height - offset.dy,
);
}
// show on left
if (_offset.dx - editorOffset.dx > editorWidth / 2) {
_alignment = _alignment == Alignment.topLeft
? Alignment.topRight
: Alignment.bottomRight;
final x = editorWidth - _offset.dx + editorOffset.dx;
final limitX = editorWidth - menuWidth + editorOffset.dx;
_offset = Offset(
min(x, limitX),
_offset.dy,
);
}
}
}
@@ -1,18 +0,0 @@
import 'package:appflowy_editor/appflowy_editor.dart';
class MobileSelectionMenuItem extends SelectionMenuItem {
MobileSelectionMenuItem({
required super.getName,
required super.icon,
super.keywords = const [],
required super.handler,
this.children = const [],
super.nameBuilder,
super.deleteKeywords,
super.deleteSlash,
});
final List<SelectionMenuItem> children;
bool get isNotEmpty => children.isNotEmpty;
}
@@ -1,140 +0,0 @@
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/material.dart';
import 'mobile_selection_menu_item.dart';
class MobileSelectionMenuItemWidget extends StatelessWidget {
const MobileSelectionMenuItemWidget({
super.key,
required this.editorState,
required this.menuService,
required this.item,
required this.isSelected,
required this.selectionMenuStyle,
required this.onTap,
});
final EditorState editorState;
final SelectionMenuService menuService;
final SelectionMenuItem item;
final bool isSelected;
final MobileSelectionMenuStyle selectionMenuStyle;
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
final style = selectionMenuStyle;
final showRightArrow = item is MobileSelectionMenuItem &&
(item as MobileSelectionMenuItem).isNotEmpty;
return Container(
padding: const EdgeInsets.symmetric(horizontal: 6),
child: TextButton.icon(
icon: item.icon(
editorState,
false,
selectionMenuStyle,
),
style: ButtonStyle(
alignment: Alignment.centerLeft,
overlayColor: WidgetStateProperty.all(
style.selectionMenuItemSelectedColor,
),
backgroundColor: isSelected
? WidgetStateProperty.all(
style.selectionMenuItemSelectedColor,
)
: WidgetStateProperty.all(Colors.transparent),
),
label: Row(
children: [
item.nameBuilder?.call(item.name, style, false) ??
Text(
item.name,
textAlign: TextAlign.left,
style: TextStyle(
color: style.selectionMenuItemTextColor,
fontSize: 16.0,
),
),
if (showRightArrow) ...[
Spacer(),
Icon(
Icons.keyboard_arrow_right_rounded,
color: style.selectionMenuItemRightIconColor,
),
],
],
),
onPressed: () {
onTap.call();
item.handler(
editorState,
menuService,
context,
);
},
),
);
}
}
class MobileSelectionMenuStyle extends SelectionMenuStyle {
const MobileSelectionMenuStyle({
required super.selectionMenuBackgroundColor,
required super.selectionMenuItemTextColor,
required super.selectionMenuItemIconColor,
required super.selectionMenuItemSelectedTextColor,
required super.selectionMenuItemSelectedIconColor,
required super.selectionMenuItemSelectedColor,
required super.selectionMenuUnselectedLabelColor,
required super.selectionMenuDividerColor,
required super.selectionMenuLinkBorderColor,
required super.selectionMenuInvalidLinkColor,
required super.selectionMenuButtonColor,
required super.selectionMenuButtonTextColor,
required super.selectionMenuButtonIconColor,
required super.selectionMenuButtonBorderColor,
required super.selectionMenuTabIndicatorColor,
required this.selectionMenuItemRightIconColor,
});
final Color selectionMenuItemRightIconColor;
static const MobileSelectionMenuStyle light = MobileSelectionMenuStyle(
selectionMenuBackgroundColor: Color(0xFFFFFFFF),
selectionMenuItemTextColor: Color(0xFF1F2225),
selectionMenuItemIconColor: Color(0xFF333333),
selectionMenuItemSelectedColor: Color(0xFFF2F5F7),
selectionMenuItemRightIconColor: Color(0xB31E2022),
selectionMenuItemSelectedTextColor: Color.fromARGB(255, 56, 91, 247),
selectionMenuItemSelectedIconColor: Color.fromARGB(255, 56, 91, 247),
selectionMenuUnselectedLabelColor: Color(0xFF333333),
selectionMenuDividerColor: Color(0xFF00BCF0),
selectionMenuLinkBorderColor: Color(0xFF00BCF0),
selectionMenuInvalidLinkColor: Color(0xFFE53935),
selectionMenuButtonColor: Color(0xFF00BCF0),
selectionMenuButtonTextColor: Color(0xFF333333),
selectionMenuButtonIconColor: Color(0xFF333333),
selectionMenuButtonBorderColor: Color(0xFF00BCF0),
selectionMenuTabIndicatorColor: Color(0xFF00BCF0),
);
static const MobileSelectionMenuStyle dark = MobileSelectionMenuStyle(
selectionMenuBackgroundColor: Color(0xFF424242),
selectionMenuItemTextColor: Color(0xFFFFFFFF),
selectionMenuItemIconColor: Color(0xFFFFFFFF),
selectionMenuItemSelectedColor: Color(0xFF666666),
selectionMenuItemRightIconColor: Color(0xB3FFFFFF),
selectionMenuItemSelectedTextColor: Color(0xFF131720),
selectionMenuItemSelectedIconColor: Color(0xFF131720),
selectionMenuUnselectedLabelColor: Color(0xFFBBC3CD),
selectionMenuDividerColor: Color(0xFF3A3F44),
selectionMenuLinkBorderColor: Color(0xFF3A3F44),
selectionMenuInvalidLinkColor: Color(0xFFE53935),
selectionMenuButtonColor: Color(0xFF00BCF0),
selectionMenuButtonTextColor: Color(0xFFFFFFFF),
selectionMenuButtonIconColor: Color(0xFFFFFFFF),
selectionMenuButtonBorderColor: Color(0xFF00BCF0),
selectionMenuTabIndicatorColor: Color(0xFF00BCF0),
);
}
@@ -1,361 +0,0 @@
import 'dart:math';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/material.dart';
import 'mobile_selection_menu_item.dart';
import 'mobile_selection_menu_item_widget.dart';
import 'slash_keyboard_service_interceptor.dart';
class MobileSelectionMenuWidget extends StatefulWidget {
const MobileSelectionMenuWidget({
super.key,
required this.items,
required this.itemCountFilter,
required this.maxItemInRow,
required this.menuService,
required this.editorState,
required this.onExit,
required this.selectionMenuStyle,
required this.deleteSlashByDefault,
required this.singleColumn,
required this.startOffset,
this.nameBuilder,
});
final List<SelectionMenuItem> items;
final int itemCountFilter;
final int maxItemInRow;
final SelectionMenuService menuService;
final EditorState editorState;
final VoidCallback onExit;
final MobileSelectionMenuStyle selectionMenuStyle;
final bool deleteSlashByDefault;
final bool singleColumn;
final int startOffset;
final SelectionMenuItemNameBuilder? nameBuilder;
@override
State<MobileSelectionMenuWidget> createState() =>
_MobileSelectionMenuWidgetState();
}
class _MobileSelectionMenuWidgetState extends State<MobileSelectionMenuWidget> {
final _focusNode = FocusNode(debugLabel: 'popup_list_widget');
List<SelectionMenuItem> _showingItems = [];
int _searchCounter = 0;
EditorState get editorState => widget.editorState;
SelectionMenuService get menuService => widget.menuService;
String _keyword = '';
String get keyword => _keyword;
int selectedIndex = 0;
late AppFlowyKeyboardServiceInterceptor keyboardInterceptor;
List<SelectionMenuItem> get filterItems {
final List<SelectionMenuItem> items = [];
for (final item in widget.items) {
if (item is MobileSelectionMenuItem) {
for (final childItem in item.children) {
items.add(childItem);
}
} else {
items.add(item);
}
}
return items;
}
set keyword(String newKeyword) {
_keyword = newKeyword;
// Search items according to the keyword, and calculate the length of
// the longest keyword, which is used to dismiss the selection_service.
var maxKeywordLength = 0;
final items = newKeyword.isEmpty
? widget.items
: filterItems
.where(
(item) => item.allKeywords.any((keyword) {
final value = keyword.contains(newKeyword.toLowerCase());
if (value) {
maxKeywordLength = max(maxKeywordLength, keyword.length);
}
return value;
}),
)
.toList(growable: false);
AppFlowyEditorLog.ui.debug('$items');
if (keyword.length >= maxKeywordLength + 2 &&
!(widget.deleteSlashByDefault && _searchCounter < 2)) {
return widget.onExit();
}
_showingItems = items;
refreshSelectedIndex();
if (_showingItems.isEmpty) {
_searchCounter++;
} else {
_searchCounter = 0;
}
}
@override
void initState() {
super.initState();
_showingItems = buildInitialItems();
keepEditorFocusNotifier.increase();
WidgetsBinding.instance.addPostFrameCallback((_) {
_focusNode.requestFocus();
});
keyboardInterceptor = SlashKeyboardServiceInterceptor(
onDelete: () async {
if (!mounted) return false;
final hasItemsChanged = !isInitialItems();
if (keyword.isEmpty && hasItemsChanged) {
_showingItems = buildInitialItems();
refreshSelectedIndex();
return true;
}
return false;
},
onEnter: () {
if (!mounted) return;
if (_showingItems.isEmpty) return;
final item = _showingItems[selectedIndex];
if (item is MobileSelectionMenuItem) {
selectedIndex = 0;
item.onSelected?.call();
} else {
item.handler(
editorState,
menuService,
context,
);
}
},
);
editorState.service.keyboardService
?.registerInterceptor(keyboardInterceptor);
editorState.selectionNotifier.addListener(onSelectionChanged);
}
@override
void dispose() {
editorState.service.keyboardService
?.unregisterInterceptor(keyboardInterceptor);
editorState.selectionNotifier.removeListener(onSelectionChanged);
_focusNode.dispose();
keepEditorFocusNotifier.decrease();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Focus(
focusNode: _focusNode,
child: DecoratedBox(
decoration: BoxDecoration(
color: widget.selectionMenuStyle.selectionMenuBackgroundColor,
boxShadow: [
BoxShadow(
blurRadius: 5,
spreadRadius: 1,
color: Colors.black.withValues(alpha: 0.1),
),
],
borderRadius: BorderRadius.circular(6.0),
),
child: _showingItems.isEmpty
? _buildNoResultsWidget(context)
: _buildResultsWidget(
context,
_showingItems,
widget.itemCountFilter,
),
),
);
}
void onSelectionChanged() {
final selection = editorState.selection;
if (selection == null) {
widget.onExit();
return;
}
if (!selection.isCollapsed) {
widget.onExit();
return;
}
final startOffset = widget.startOffset;
final endOffset = selection.end.offset;
if (endOffset < startOffset) {
widget.onExit();
return;
}
final node = editorState.getNodeAtPath(selection.start.path);
final text = node?.delta?.toPlainText() ?? '';
final search = text.substring(startOffset, endOffset);
keyword = search;
}
Widget _buildResultsWidget(
BuildContext buildContext,
List<SelectionMenuItem> items,
int itemCountFilter,
) {
if (widget.singleColumn) {
final List<Widget> itemWidgets = [];
for (var i = 0; i < items.length; i++) {
final item = items[i];
itemWidgets.add(
GestureDetector(
onTapDown: (e) {
setState(() {
selectedIndex = i;
});
},
child: MobileSelectionMenuItemWidget(
item: item,
isSelected: i == selectedIndex,
editorState: editorState,
menuService: menuService,
selectionMenuStyle: widget.selectionMenuStyle,
onTap: () {
if (item is MobileSelectionMenuItem) refreshSelectedIndex();
},
),
),
);
}
return ConstrainedBox(
constraints: const BoxConstraints(
maxHeight: 192,
minWidth: 240,
maxWidth: 240,
),
child: ListView(
shrinkWrap: true,
padding: EdgeInsets.zero,
children: itemWidgets,
),
);
} else {
final List<Widget> columns = [];
List<Widget> itemWidgets = [];
// apply item count filter
if (itemCountFilter > 0) {
items = items.take(itemCountFilter).toList();
}
for (var i = 0; i < items.length; i++) {
final item = items[i];
if (i != 0 && i % (widget.maxItemInRow) == 0) {
columns.add(
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: itemWidgets,
),
);
itemWidgets = [];
}
itemWidgets.add(
MobileSelectionMenuItemWidget(
item: item,
isSelected: false,
editorState: editorState,
menuService: menuService,
selectionMenuStyle: widget.selectionMenuStyle,
onTap: () {
if (item is MobileSelectionMenuItem) refreshSelectedIndex();
},
),
);
}
if (itemWidgets.isNotEmpty) {
columns.add(
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: itemWidgets,
),
);
itemWidgets = [];
}
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: columns,
);
}
}
void refreshSelectedIndex() {
if (!mounted) return;
setState(() {
selectedIndex = 0;
});
}
Widget _buildNoResultsWidget(BuildContext context) {
return const Padding(
padding: EdgeInsets.all(8.0),
child: SizedBox(
width: 140,
child: Material(
child: Text(
"No results",
style: TextStyle(fontSize: 18.0, color: Colors.grey),
textAlign: TextAlign.center,
),
),
),
);
}
List<SelectionMenuItem> buildInitialItems() {
final List<SelectionMenuItem> items = [];
for (final item in widget.items) {
if (item is MobileSelectionMenuItem) {
item.onSelected = () {
if (mounted) {
setState(() {
_showingItems = item.children
.map((e) => e..onSelected = widget.onExit)
.toList();
});
}
};
}
items.add(item);
}
return items;
}
bool isInitialItems() {
if (_showingItems.length != widget.items.length) return false;
int i = 0;
for (final item in _showingItems) {
final widgetItem = widget.items[i];
if (widgetItem.name != item.name) return false;
i++;
}
return true;
}
}
@@ -1,42 +0,0 @@
import 'package:appflowy/plugins/document/presentation/editor_plugins/keyboard_interceptor/keyboard_interceptor.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
class SlashKeyboardServiceInterceptor extends EditorKeyboardInterceptor {
SlashKeyboardServiceInterceptor({
required this.onDelete,
required this.onEnter,
});
final AsyncValueGetter<bool> onDelete;
final VoidCallback onEnter;
@override
Future<bool> interceptDelete(
TextEditingDeltaDeletion deletion,
EditorState editorState,
) async {
final intercept = await onDelete.call();
if (intercept) {
return true;
} else {
return super.interceptDelete(deletion, editorState);
}
}
@override
Future<bool> interceptInsert(
TextEditingDeltaInsertion insertion,
EditorState editorState,
List<CharacterShortcutEvent> characterShortcutEvents,
) async {
final text = insertion.textInserted;
if (text.contains('\n')) {
onEnter.call();
return true;
}
return super
.interceptInsert(insertion, editorState, characterShortcutEvents);
}
}
@@ -1,106 +0,0 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart';
import 'package:appflowy/mobile/presentation/setting/widgets/mobile_setting_group_widget.dart';
import 'package:appflowy/mobile/presentation/setting/widgets/mobile_setting_item_widget.dart';
import 'package:appflowy/mobile/presentation/widgets/flowy_option_tile.dart';
import 'package:appflowy/workspace/application/settings/ai/settings_ai_bloc.dart';
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-user/workspace.pbenum.dart';
import 'package:collection/collection.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
class AiSettingsGroup extends StatelessWidget {
const AiSettingsGroup({
super.key,
required this.userProfile,
required this.workspaceId,
this.currentWorkspaceMemberRole,
});
final UserProfilePB userProfile;
final String workspaceId;
final AFRolePB? currentWorkspaceMemberRole;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return BlocProvider(
create: (context) => SettingsAIBloc(
userProfile,
workspaceId,
currentWorkspaceMemberRole,
)..add(const SettingsAIEvent.started()),
child: BlocBuilder<SettingsAIBloc, SettingsAIState>(
builder: (context, state) {
return MobileSettingGroup(
groupTitle: LocaleKeys.settings_aiPage_title.tr(),
settingItemList: [
MobileSettingItem(
name: LocaleKeys.settings_aiPage_keys_llmModelType.tr(),
trailing: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 200),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Flexible(
child: FlowyText(
state.selectedAIModel,
color: theme.colorScheme.onSurface,
overflow: TextOverflow.ellipsis,
),
),
const Icon(Icons.chevron_right),
],
),
),
onTap: () => _onLLMModelTypeTap(context, state),
),
// enable AI search if needed
// MobileSettingItem(
// name: LocaleKeys.settings_aiPage_keys_enableAISearchTitle.tr(),
// trailing: const Icon(
// Icons.chevron_right,
// ),
// onTap: () => context.push(AppFlowyCloudPage.routeName),
// ),
],
);
},
),
);
}
void _onLLMModelTypeTap(BuildContext context, SettingsAIState state) {
final availableModels = state.availableModels;
showMobileBottomSheet(
context,
showHeader: true,
showDragHandle: true,
showDivider: false,
title: LocaleKeys.settings_aiPage_keys_llmModelType.tr(),
builder: (_) {
return Column(
children: availableModels
.mapIndexed(
(index, model) => FlowyOptionTile.checkbox(
text: model,
showTopBorder: index == 0,
isSelected: state.selectedAIModel == model,
onTap: () {
context
.read<SettingsAIBloc>()
.add(SettingsAIEvent.selectModel(model));
context.pop();
},
),
)
.toList(),
);
},
);
}
}
@@ -83,6 +83,7 @@ class RTLSetting extends StatelessWidget {
case AppFlowyTextDirection.rtl:
return LocaleKeys.settings_appearance_textDirection_rtl.tr();
case AppFlowyTextDirection.ltr:
default:
return LocaleKeys.settings_appearance_textDirection_ltr.tr();
}
}
@@ -12,7 +12,6 @@ class MobileQuickActionButton extends StatelessWidget {
this.iconColor,
this.iconSize,
this.enable = true,
this.rightIconBuilder,
});
final VoidCallback onTap;
@@ -22,39 +21,34 @@ class MobileQuickActionButton extends StatelessWidget {
final Color? iconColor;
final Size? iconSize;
final bool enable;
final WidgetBuilder? rightIconBuilder;
@override
Widget build(BuildContext context) {
final iconSize = this.iconSize ?? const Size.square(18);
return Opacity(
opacity: enable ? 1.0 : 0.5,
child: InkWell(
onTap: enable ? onTap : null,
overlayColor:
enable ? null : const WidgetStatePropertyAll(Colors.transparent),
splashColor: Colors.transparent,
child: Container(
height: 52,
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
children: [
FlowySvg(
icon,
size: iconSize,
color: iconColor,
return InkWell(
onTap: enable ? onTap : null,
overlayColor:
enable ? null : const WidgetStatePropertyAll(Colors.transparent),
splashColor: Colors.transparent,
child: Container(
height: 52,
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
children: [
FlowySvg(
icon,
size: iconSize,
color: enable ? iconColor : Theme.of(context).disabledColor,
),
HSpace(30 - iconSize.width),
Expanded(
child: FlowyText.regular(
text,
fontSize: 16,
color: enable ? textColor : Theme.of(context).disabledColor,
),
HSpace(30 - iconSize.width),
Expanded(
child: FlowyText.regular(
text,
fontSize: 16,
color: textColor,
),
),
if (rightIconBuilder != null) rightIconBuilder!(context),
],
),
),
],
),
),
);
@@ -37,7 +37,6 @@ class FlowyOptionTile extends StatelessWidget {
this.backgroundColor,
this.fontFamily,
this.height,
this.enable = true,
});
factory FlowyOptionTile.text({
@@ -50,7 +49,6 @@ class FlowyOptionTile extends StatelessWidget {
Widget? trailing,
VoidCallback? onTap,
double? height,
bool enable = true,
}) {
return FlowyOptionTile._(
type: FlowyOptionTileType.text,
@@ -63,7 +61,6 @@ class FlowyOptionTile extends StatelessWidget {
leading: leftIcon,
trailing: trailing,
height: height,
enable: enable,
);
}
@@ -80,7 +77,6 @@ class FlowyOptionTile extends StatelessWidget {
Widget? trailing,
String? textFieldHintText,
bool autofocus = false,
bool enable = true,
}) {
return FlowyOptionTile._(
type: FlowyOptionTileType.textField,
@@ -94,7 +90,6 @@ class FlowyOptionTile extends StatelessWidget {
onTextChanged: onTextChanged,
onTextSubmitted: onTextSubmitted,
autofocus: autofocus,
enable: enable,
);
}
@@ -110,7 +105,6 @@ class FlowyOptionTile extends StatelessWidget {
bool showBottomBorder = true,
String? fontFamily,
Color? backgroundColor,
bool enable = true,
}) {
return FlowyOptionTile._(
key: key,
@@ -125,7 +119,6 @@ class FlowyOptionTile extends StatelessWidget {
showTopBorder: showTopBorder,
showBottomBorder: showBottomBorder,
leading: leftIcon,
enable: enable,
trailing: isSelected
? const FlowySvg(
FlowySvgs.m_blue_check_s,
@@ -143,7 +136,6 @@ class FlowyOptionTile extends StatelessWidget {
bool showTopBorder = true,
bool showBottomBorder = true,
Widget? leftIcon,
bool enable = true,
}) {
return FlowyOptionTile._(
type: FlowyOptionTileType.toggle,
@@ -154,7 +146,6 @@ class FlowyOptionTile extends StatelessWidget {
showBottomBorder: showBottomBorder,
leading: leftIcon,
trailing: _Toggle(value: isSelected, onChanged: onValueChanged),
enable: enable,
);
}
@@ -190,13 +181,11 @@ class FlowyOptionTile extends StatelessWidget {
final double? height;
final bool enable;
@override
Widget build(BuildContext context) {
final leadingWidget = _buildLeading();
Widget child = FlowyOptionDecorateBox(
final child = FlowyOptionDecorateBox(
color: backgroundColor,
showTopBorder: showTopBorder,
showBottomBorder: showBottomBorder,
@@ -220,21 +209,12 @@ class FlowyOptionTile extends StatelessWidget {
if (type == FlowyOptionTileType.checkbox ||
type == FlowyOptionTileType.toggle ||
type == FlowyOptionTileType.text) {
child = GestureDetector(
return GestureDetector(
onTap: onTap,
child: child,
);
}
if (!enable) {
child = Opacity(
opacity: 0.5,
child: IgnorePointer(
child: child,
),
);
}
return child;
}
@@ -319,7 +299,7 @@ class _Toggle extends StatelessWidget {
fit: BoxFit.fill,
child: CupertinoSwitch(
value: value,
activeTrackColor: Theme.of(context).colorScheme.primary,
activeColor: Theme.of(context).colorScheme.primary,
onChanged: onChanged,
),
),
@@ -99,7 +99,7 @@ Future<T?> showFlowyCupertinoConfirmDialog<T>({
}) {
return showDialog(
context: context ?? AppGlobals.context,
barrierColor: Colors.black.withValues(alpha: 0.25),
barrierColor: Colors.black.withOpacity(0.25),
builder: (context) => CupertinoAlertDialog(
title: FlowyText.medium(
title,
@@ -10,15 +10,12 @@ import 'package:appflowy_result/appflowy_result.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'ai_entities.dart';
part 'ai_prompt_input_bloc.freezed.dart';
class AIPromptInputBloc extends Bloc<AIPromptInputEvent, AIPromptInputState> {
AIPromptInputBloc({
required PredefinedFormat? predefinedFormat,
}) : _listener = LocalLLMListener(),
super(AIPromptInputState.initial(predefinedFormat)) {
AIPromptInputBloc()
: _listener = LocalLLMListener(),
super(AIPromptInputState.initial()) {
_dispatch();
_startListening();
_init();
@@ -44,7 +41,6 @@ class AIPromptInputBloc extends Bloc<AIPromptInputEvent, AIPromptInputState> {
final aiType = chatState.pluginState.state == RunningStatePB.Running
? AIType.localAI
: AIType.appflowyAI;
emit(
state.copyWith(
aiType: aiType,
@@ -69,24 +65,6 @@ class AIPromptInputBloc extends Bloc<AIPromptInputEvent, AIPromptInputState> {
),
);
},
toggleShowPredefinedFormat: () {
final predefinedFormat =
!state.showPredefinedFormats && state.predefinedFormat == null
? PredefinedFormat(
imageFormat: ImageFormat.text,
textFormat: TextFormat.paragraph,
)
: null;
emit(
state.copyWith(
showPredefinedFormats: !state.showPredefinedFormats,
predefinedFormat: predefinedFormat,
),
);
},
updatePredefinedFormat: (format) {
emit(state.copyWith(predefinedFormat: format));
},
attachFile: (filePath, fileName) {
final newFile = ChatFile.fromFilePath(filePath);
if (newFile != null) {
@@ -174,11 +152,6 @@ class AIPromptInputEvent with _$AIPromptInputEvent {
const factory AIPromptInputEvent.updatePluginState(
LocalAIPluginStatePB chatState,
) = _UpdatePluginState;
const factory AIPromptInputEvent.toggleShowPredefinedFormat() =
_ToggleShowPredefinedFormat;
const factory AIPromptInputEvent.updatePredefinedFormat(
PredefinedFormat format,
) = _UpdatePredefinedFormat;
const factory AIPromptInputEvent.attachFile(
String filePath,
String fileName,
@@ -194,19 +167,14 @@ class AIPromptInputState with _$AIPromptInputState {
const factory AIPromptInputState({
required AIType aiType,
required bool supportChatWithFile,
required bool showPredefinedFormats,
required PredefinedFormat? predefinedFormat,
required LocalAIChatPB? chatState,
required List<ChatFile> attachedFiles,
required List<ViewPB> mentionedPages,
}) = _AIPromptInputState;
factory AIPromptInputState.initial(PredefinedFormat? format) =>
AIPromptInputState(
factory AIPromptInputState.initial() => const AIPromptInputState(
aiType: AIType.appflowyAI,
supportChatWithFile: false,
showPredefinedFormats: format != null,
predefinedFormat: format,
chatState: null,
attachedFiles: [],
mentionedPages: [],
@@ -1,7 +1,6 @@
import 'dart:async';
import 'dart:collection';
import 'package:appflowy/ai/ai.dart';
import 'package:appflowy/util/int64_extension.dart';
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/log.dart';
@@ -11,7 +10,6 @@ import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_result/appflowy_result.dart';
import 'package:collection/collection.dart';
import 'package:fixnum/fixnum.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_chat_core/flutter_chat_core.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
@@ -30,7 +28,6 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
required this.userId,
}) : chatController = InMemoryChatController(),
listener = ChatMessageListener(chatId: chatId),
selectedSourcesNotifier = ValueNotifier([]),
super(ChatState.initial()) {
_startListening();
_dispatch();
@@ -41,7 +38,7 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
final String chatId;
final String userId;
final ChatMessageListener listener;
final ValueNotifier<List<String>> selectedSourcesNotifier;
final ChatController chatController;
/// The last streaming message id
@@ -71,7 +68,7 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
await listener.stop();
final request = ViewIdPB(value: chatId);
unawaited(FolderEventCloseView(request).send());
selectedSourcesNotifier.dispose();
return super.close();
}
@@ -254,10 +251,12 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
);
},
didReceiveChatSettings: (settings) {
selectedSourcesNotifier.value = settings.ragIds;
emit(
state.copyWith(selectedSourceIds: settings.ragIds),
);
},
updateSelectedSources: (selectedSourcesIds) async {
selectedSourcesNotifier.value = [...selectedSourcesIds];
emit(state.copyWith(selectedSourceIds: selectedSourcesIds));
final payload = UpdateChatSettingsPB(
chatId: ChatId(value: chatId),
@@ -666,12 +665,14 @@ class ChatEvent with _$ChatEvent {
@freezed
class ChatState with _$ChatState {
const factory ChatState({
required List<String> selectedSourceIds,
required LoadChatMessageStatus loadingState,
required PromptResponseState promptResponseState,
required bool clearErrorMessages,
}) = _ChatState;
factory ChatState.initial() => const ChatState(
selectedSourceIds: [],
loadingState: LoadChatMessageStatus.loading,
promptResponseState: PromptResponseState.ready,
clearErrorMessages: false,
@@ -1,8 +1,11 @@
import 'dart:io';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy_backend/protobuf/flowy-ai/protobuf.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:equatable/equatable.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:path/path.dart' as path;
@@ -137,3 +140,87 @@ enum LoadChatMessageStatus {
loadingRemote,
ready,
}
class PredefinedFormat extends Equatable {
const PredefinedFormat({
required this.imageFormat,
required this.textFormat,
});
const PredefinedFormat.auto()
: imageFormat = ImageFormat.text,
textFormat = TextFormat.auto;
final ImageFormat imageFormat;
final TextFormat? textFormat;
PredefinedFormatPB toPB() {
return PredefinedFormatPB(
imageFormat: switch (imageFormat) {
ImageFormat.text => ResponseImageFormatPB.TextOnly,
ImageFormat.image => ResponseImageFormatPB.ImageOnly,
ImageFormat.textAndImage => ResponseImageFormatPB.TextAndImage,
},
textFormat: switch (textFormat) {
TextFormat.auto => ResponseTextFormatPB.Paragraph,
TextFormat.bulletList => ResponseTextFormatPB.BulletedList,
TextFormat.numberedList => ResponseTextFormatPB.NumberedList,
TextFormat.table => ResponseTextFormatPB.Table,
_ => null,
},
);
}
@override
List<Object?> get props => [imageFormat, textFormat];
}
enum ImageFormat {
text,
image,
textAndImage;
bool get hasText => this == text || this == textAndImage;
FlowySvgData get icon {
return switch (this) {
ImageFormat.text => FlowySvgs.ai_text_s,
ImageFormat.image => FlowySvgs.ai_image_s,
ImageFormat.textAndImage => FlowySvgs.ai_text_image_s,
};
}
String get i18n {
return switch (this) {
ImageFormat.text => LocaleKeys.chat_changeFormat_textOnly.tr(),
ImageFormat.image => LocaleKeys.chat_changeFormat_imageOnly.tr(),
ImageFormat.textAndImage =>
LocaleKeys.chat_changeFormat_textAndImage.tr(),
};
}
}
enum TextFormat {
auto,
bulletList,
numberedList,
table;
FlowySvgData get icon {
return switch (this) {
TextFormat.auto => FlowySvgs.ai_paragraph_s,
TextFormat.bulletList => FlowySvgs.ai_list_s,
TextFormat.numberedList => FlowySvgs.ai_number_list_s,
TextFormat.table => FlowySvgs.ai_table_s,
};
}
String get i18n {
return switch (this) {
TextFormat.auto => LocaleKeys.chat_changeFormat_text.tr(),
TextFormat.bulletList => LocaleKeys.chat_changeFormat_bullet.tr(),
TextFormat.numberedList => LocaleKeys.chat_changeFormat_number.tr(),
TextFormat.table => LocaleKeys.chat_changeFormat_table.tr(),
};
}
}
@@ -113,8 +113,9 @@ class ChatSettingsCubit extends Cubit<ChatSettingsState> {
String filter = '';
void updateSelectedSources(List<String> newSelectedSourceIds) {
selectedSourceIds.clear();
selectedSourceIds.addAll(newSelectedSourceIds);
selectedSourceIds
..clear
..addAll(newSelectedSourceIds);
}
void refreshSources(List<ViewPB> spaceViews, ViewPB? currentSpace) async {
@@ -1,6 +1,5 @@
import 'dart:io';
import 'package:appflowy/ai/ai.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/ai_chat/presentation/chat_message_selector_banner.dart';
import 'package:appflowy/workspace/application/view/view_service.dart';
@@ -20,12 +19,14 @@ import 'package:string_validator/string_validator.dart';
import 'package:universal_platform/universal_platform.dart';
import 'package:url_launcher/url_launcher.dart';
import 'application/ai_prompt_input_bloc.dart';
import 'application/chat_bloc.dart';
import 'application/chat_entity.dart';
import 'application/chat_member_bloc.dart';
import 'application/chat_select_message_bloc.dart';
import 'application/chat_message_stream.dart';
import 'presentation/animated_chat_list.dart';
import 'presentation/chat_input/desktop_chat_input.dart';
import 'presentation/chat_input/mobile_chat_input.dart';
import 'presentation/chat_related_question.dart';
import 'presentation/chat_welcome_page.dart';
@@ -70,14 +71,7 @@ class AIChatPage extends StatelessWidget {
),
/// [AIPromptInputBloc] is used to handle the user prompt
BlocProvider(
create: (_) => AIPromptInputBloc(
predefinedFormat: PredefinedFormat(
imageFormat: ImageFormat.text,
textFormat: TextFormat.bulletList,
),
),
),
BlocProvider(create: (_) => AIPromptInputBloc()),
BlocProvider(create: (_) => ChatMemberBloc()),
],
child: Builder(
@@ -127,24 +121,21 @@ class _ChatContentPage extends StatelessWidget {
child: Align(
alignment: Alignment.topCenter,
child: _wrapConstraints(
SelectionArea(
child: ScrollConfiguration(
behavior: ScrollConfiguration.of(context)
.copyWith(scrollbars: false),
child: Chat(
chatController:
context.read<ChatBloc>().chatController,
user: User(id: userProfile.id.toString()),
darkTheme:
ChatTheme.fromThemeData(Theme.of(context)),
theme: ChatTheme.fromThemeData(Theme.of(context)),
builders: Builders(
inputBuilder: (_) => const SizedBox.shrink(),
textMessageBuilder: _buildTextMessage,
chatMessageBuilder: _buildChatMessage,
scrollToBottomBuilder: _buildScrollToBottom,
chatAnimatedListBuilder: _buildChatAnimatedList,
),
ScrollConfiguration(
behavior: ScrollConfiguration.of(context)
.copyWith(scrollbars: false),
child: Chat(
chatController:
context.read<ChatBloc>().chatController,
user: User(id: userProfile.id.toString()),
darkTheme: ChatTheme.fromThemeData(Theme.of(context)),
theme: ChatTheme.fromThemeData(Theme.of(context)),
builders: Builders(
inputBuilder: (_) => const SizedBox.shrink(),
textMessageBuilder: _buildTextMessage,
chatMessageBuilder: _buildChatMessage,
scrollToBottomBuilder: _buildScrollToBottom,
chatAnimatedListBuilder: _buildChatAnimatedList,
),
),
),
@@ -152,7 +143,7 @@ class _ChatContentPage extends StatelessWidget {
),
),
_wrapConstraints(
_Input(view: view),
_builtInput(context),
),
],
),
@@ -190,25 +181,26 @@ class _ChatContentPage extends StatelessWidget {
return RelatedQuestionList(
relatedQuestions: message.metadata!['questions'],
onQuestionSelected: (question) {
final bloc = context.read<AIPromptInputBloc>();
final showPredefinedFormats = bloc.state.showPredefinedFormats;
final predefinedFormat = bloc.state.predefinedFormat;
context.read<ChatBloc>().add(
ChatEvent.sendMessage(
message: question,
format: showPredefinedFormats ? predefinedFormat : null,
),
);
context
.read<ChatBloc>()
.add(ChatEvent.sendMessage(message: question));
},
);
}
if (message.author.id == userProfile.id.toString() ||
isOtherUserMessage(message)) {
if (message.author.id == userProfile.id.toString()) {
return ChatUserMessageWidget(
user: message.author,
message: message,
isCurrentUser: true,
);
}
if (isOtherUserMessage(message)) {
return ChatUserMessageWidget(
user: message.author,
message: message,
isCurrentUser: false,
);
}
@@ -291,16 +283,7 @@ class _ChatContentPage extends StatelessWidget {
return ChatWelcomePage(
userProfile: userProfile,
onSelectedQuestion: (question) {
final aiPromptInputBloc = context.read<AIPromptInputBloc>();
final showPredefinedFormats =
aiPromptInputBloc.state.showPredefinedFormats;
final predefinedFormat = aiPromptInputBloc.state.predefinedFormat;
bloc.add(
ChatEvent.sendMessage(
message: question,
format: showPredefinedFormats ? predefinedFormat : null,
),
);
bloc.add(ChatEvent.sendMessage(message: question));
},
);
}
@@ -322,6 +305,86 @@ class _ChatContentPage extends StatelessWidget {
);
}
Widget _builtInput(BuildContext context) {
return BlocSelector<ChatSelectMessageBloc, ChatSelectMessageState, bool>(
selector: (state) => state.isSelectingMessages,
builder: (context, isSelectingMessages) {
return AnimatedSwitcher(
duration: const Duration(milliseconds: 150),
transitionBuilder: (child, animation) {
return SizeTransition(
sizeFactor: animation,
axisAlignment: -1,
child: child,
);
},
child: isSelectingMessages
? const SizedBox.shrink()
: Padding(
padding: AIChatUILayout.safeAreaInsets(context),
child: BlocSelector<ChatBloc, ChatState, bool>(
selector: (state) {
return state.promptResponseState ==
PromptResponseState.ready;
},
builder: (context, canSendMessage) {
final chatBloc = context.read<ChatBloc>();
return UniversalPlatform.isDesktop
? DesktopChatInput(
chatId: view.id,
isStreaming: !canSendMessage,
onStopStreaming: () {
chatBloc.add(const ChatEvent.stopStream());
},
onSubmitted: (text, format, metadata) {
chatBloc.add(
ChatEvent.sendMessage(
message: text,
format: format,
metadata: metadata,
),
);
},
onUpdateSelectedSources: (ids) {
chatBloc.add(
ChatEvent.updateSelectedSources(
selectedSourcesIds: ids,
),
);
},
)
: MobileChatInput(
chatId: view.id,
isStreaming: !canSendMessage,
onStopStreaming: () {
chatBloc.add(const ChatEvent.stopStream());
},
onSubmitted: (text, format, metadata) {
chatBloc.add(
ChatEvent.sendMessage(
message: text,
format: format,
metadata: metadata,
),
);
},
onUpdateSelectedSources: (ids) {
chatBloc.add(
ChatEvent.updateSelectedSources(
selectedSourcesIds: ids,
),
);
},
);
},
),
),
);
},
);
}
void _onSelectMetadata(
BuildContext context,
ChatMessageRefSource metadata,
@@ -348,94 +411,3 @@ class _ChatContentPage extends StatelessWidget {
}
}
}
class _Input extends StatelessWidget {
const _Input({
required this.view,
});
final ViewPB view;
@override
Widget build(BuildContext context) {
return BlocSelector<ChatSelectMessageBloc, ChatSelectMessageState, bool>(
selector: (state) => state.isSelectingMessages,
builder: (context, isSelectingMessages) {
return AnimatedSwitcher(
duration: const Duration(milliseconds: 150),
transitionBuilder: (child, animation) {
return SizeTransition(
sizeFactor: animation,
axisAlignment: -1,
child: child,
);
},
child: isSelectingMessages
? const SizedBox.shrink()
: Padding(
padding: AIChatUILayout.safeAreaInsets(context),
child: BlocSelector<ChatBloc, ChatState, bool>(
selector: (state) {
return state.promptResponseState ==
PromptResponseState.ready;
},
builder: (context, canSendMessage) {
final chatBloc = context.read<ChatBloc>();
return UniversalPlatform.isDesktop
? DesktopPromptInput(
isStreaming: !canSendMessage,
onStopStreaming: () {
chatBloc.add(const ChatEvent.stopStream());
},
onSubmitted: (text, format, metadata) {
chatBloc.add(
ChatEvent.sendMessage(
message: text,
format: format,
metadata: metadata,
),
);
},
selectedSourcesNotifier:
chatBloc.selectedSourcesNotifier,
onUpdateSelectedSources: (ids) {
chatBloc.add(
ChatEvent.updateSelectedSources(
selectedSourcesIds: ids,
),
);
},
)
: MobileChatInput(
isStreaming: !canSendMessage,
onStopStreaming: () {
chatBloc.add(const ChatEvent.stopStream());
},
onSubmitted: (text, format, metadata) {
chatBloc.add(
ChatEvent.sendMessage(
message: text,
format: format,
metadata: metadata,
),
);
},
selectedSourcesNotifier:
chatBloc.selectedSourcesNotifier,
onUpdateSelectedSources: (ids) {
chatBloc.add(
ChatEvent.updateSelectedSources(
selectedSourcesIds: ids,
),
);
},
);
},
),
),
);
},
);
}
}
@@ -7,9 +7,10 @@ import 'package:diffutil_dart/diffutil.dart' as diffutil;
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_chat_core/flutter_chat_core.dart';
import 'package:provider/provider.dart';
import 'package:flutter_chat_ui/src/scroll_to_bottom.dart';
import 'package:flutter_chat_ui/src/utils/message_list_diff.dart';
import 'package:provider/provider.dart';
class ChatAnimatedListReversed extends StatefulWidget {
const ChatAnimatedListReversed({
@@ -75,8 +75,7 @@ class ChatEditorStyleCustomizer extends EditorStyleCustomizer {
fontSize: fontSize,
fontWeight: FontWeight.normal,
color: Colors.red,
backgroundColor:
theme.colorScheme.inverseSurface.withValues(alpha: 0.8),
backgroundColor: theme.colorScheme.inverseSurface.withOpacity(0.8),
),
),
),
@@ -145,7 +144,7 @@ class ChatEditorStyleCustomizer extends EditorStyleCustomizer {
return TextStyle(
fontFamily: defaultFontFamily,
height: 1.5,
color: AFThemeExtension.of(context).onBackground.withValues(alpha: 0.6),
color: AFThemeExtension.of(context).onBackground.withOpacity(0.6),
);
}
}
@@ -1,42 +1,41 @@
import 'package:appflowy/ai/ai.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/ai_chat/application/ai_prompt_input_bloc.dart';
import 'package:appflowy/plugins/ai_chat/application/chat_entity.dart';
import 'package:appflowy/plugins/ai_chat/application/chat_input_control_cubit.dart';
import 'package:appflowy/plugins/ai_chat/presentation/layout_define.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/util/theme_extension.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:extended_text_field/extended_text_field.dart';
import 'package:flowy_infra/file_picker/file_picker_service.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class DesktopPromptInput extends StatefulWidget {
const DesktopPromptInput({
import '../layout_define.dart';
class DesktopChatInput extends StatefulWidget {
const DesktopChatInput({
super.key,
required this.chatId,
required this.isStreaming,
required this.onStopStreaming,
required this.onSubmitted,
required this.selectedSourcesNotifier,
required this.onUpdateSelectedSources,
this.hideDecoration = false,
});
final String chatId;
final bool isStreaming;
final void Function() onStopStreaming;
final void Function(String, PredefinedFormat?, Map<String, dynamic>)
onSubmitted;
final ValueNotifier<List<String>> selectedSourcesNotifier;
final void Function(List<String>) onUpdateSelectedSources;
final bool hideDecoration;
@override
State<DesktopPromptInput> createState() => _DesktopPromptInputState();
State<DesktopChatInput> createState() => _DesktopChatInputState();
}
class _DesktopPromptInputState extends State<DesktopPromptInput> {
class _DesktopChatInputState extends State<DesktopChatInput> {
final textFieldKey = GlobalKey();
final layerLink = LayerLink();
final overlayController = OverlayPortalController();
@@ -44,6 +43,11 @@ class _DesktopPromptInputState extends State<DesktopPromptInput> {
final focusNode = FocusNode();
final textController = TextEditingController();
bool showPredefinedFormatSection = true;
PredefinedFormat predefinedFormat = const PredefinedFormat(
imageFormat: ImageFormat.text,
textFormat: TextFormat.bulletList,
);
late SendButtonState sendButtonState;
bool isComposing = false;
@@ -53,15 +57,13 @@ class _DesktopPromptInputState extends State<DesktopPromptInput> {
textController.addListener(handleTextControllerChanged);
// refresh border color on focus change and hide menu when lost focus
focusNode.addListener(
() {
if (!widget.hideDecoration) {
setState(() {}); // refresh border color
}
() => setState(() {
if (!focusNode.hasFocus) {
cancelMentionPage(); // hide menu when lost focus
cancelMentionPage();
}
},
}),
);
updateSendButtonState();
@@ -110,7 +112,15 @@ class _DesktopPromptInputState extends State<DesktopPromptInput> {
);
},
child: DecoratedBox(
decoration: decoration(context),
decoration: BoxDecoration(
border: Border.all(
color: focusNode.hasFocus
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.outline,
width: focusNode.hasFocus ? 1.5 : 1.0,
),
borderRadius: const BorderRadius.all(Radius.circular(12.0)),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
@@ -122,6 +132,7 @@ class _DesktopPromptInputState extends State<DesktopPromptInput> {
),
child: TextFieldTapRegion(
child: PromptInputFile(
chatId: widget.chatId,
onDeleted: (file) => context
.read<AIPromptInputBloc>()
.add(AIPromptInputEvent.removeFile(file)),
@@ -129,63 +140,53 @@ class _DesktopPromptInputState extends State<DesktopPromptInput> {
),
),
const VSpace(4.0),
BlocBuilder<AIPromptInputBloc, AIPromptInputState>(
builder: (context, state) {
return Stack(
children: [
ConstrainedBox(
constraints: getTextFieldConstraints(
state.showPredefinedFormats,
),
child: inputTextField(),
),
if (state.showPredefinedFormats)
Positioned.fill(
bottom: null,
child: TextFieldTapRegion(
child: Padding(
padding: const EdgeInsetsDirectional.only(
start: 8.0,
),
child: ChangeFormatBar(
predefinedFormat: state.predefinedFormat,
spacing: 4.0,
onSelectPredefinedFormat: (format) =>
context.read<AIPromptInputBloc>().add(
AIPromptInputEvent
.updatePredefinedFormat(
format,
),
),
),
),
),
),
Positioned.fill(
top: null,
child: TextFieldTapRegion(
child: _PromptBottomActions(
showPredefinedFormats:
state.showPredefinedFormats,
onTogglePredefinedFormatSection: () =>
context.read<AIPromptInputBloc>().add(
AIPromptInputEvent
.toggleShowPredefinedFormat(),
),
onStartMention: startMentionPageFromButton,
sendButtonState: sendButtonState,
onSendPressed: handleSend,
onStopStreaming: widget.onStopStreaming,
selectedSourcesNotifier:
widget.selectedSourcesNotifier,
onUpdateSelectedSources:
widget.onUpdateSelectedSources,
Stack(
children: [
Container(
constraints: getTextFieldConstraints(),
child: inputTextField(),
),
if (showPredefinedFormatSection)
Positioned.fill(
bottom: null,
child: TextFieldTapRegion(
child: Padding(
padding:
const EdgeInsetsDirectional.only(start: 8.0),
child: ChangeFormatBar(
predefinedFormat: predefinedFormat,
spacing: 4.0,
onSelectPredefinedFormat: (format) {
setState(() => predefinedFormat = format);
},
),
),
),
],
);
},
),
Positioned.fill(
top: null,
child: TextFieldTapRegion(
child: _PromptBottomActions(
textController: textController,
overlayController: overlayController,
focusNode: focusNode,
showPredefinedFormats: showPredefinedFormatSection,
predefinedFormat: predefinedFormat,
onTogglePredefinedFormatSection: () {
setState(() {
showPredefinedFormatSection =
!showPredefinedFormatSection;
});
},
sendButtonState: sendButtonState,
onSendPressed: handleSend,
onStopStreaming: widget.onStopStreaming,
onUpdateSelectedSources:
widget.onUpdateSelectedSources,
),
),
),
],
),
],
),
@@ -195,40 +196,6 @@ class _DesktopPromptInputState extends State<DesktopPromptInput> {
);
}
BoxDecoration decoration(BuildContext context) {
if (widget.hideDecoration) {
return BoxDecoration();
}
return BoxDecoration(
color: Theme.of(context).colorScheme.surface,
border: Border.all(
color: focusNode.hasFocus
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.outline,
width: focusNode.hasFocus ? 1.5 : 1.0,
),
borderRadius: const BorderRadius.all(Radius.circular(12.0)),
);
}
void startMentionPageFromButton() {
if (overlayController.isShowing) {
return;
}
if (!focusNode.hasFocus) {
focusNode.requestFocus();
}
textController.text += '@';
WidgetsBinding.instance.addPostFrameCallback((_) {
if (context.mounted) {
context
.read<ChatInputControlCubit>()
.startSearching(textController.value);
overlayController.show();
}
});
}
void cancelMentionPage() {
if (overlayController.isShowing) {
inputControlCubit.reset();
@@ -261,13 +228,9 @@ class _DesktopPromptInputState extends State<DesktopPromptInput> {
// get the attached files and mentioned pages
final metadata = context.read<AIPromptInputBloc>().consumeMetadata();
final bloc = context.read<AIPromptInputBloc>();
final showPredefinedFormats = bloc.state.showPredefinedFormats;
final predefinedFormat = bloc.state.predefinedFormat;
widget.onSubmitted(
trimmedText,
showPredefinedFormats ? predefinedFormat : null,
showPredefinedFormatSection ? predefinedFormat : null,
metadata,
);
}
@@ -367,6 +330,18 @@ class _DesktopPromptInputState extends State<DesktopPromptInput> {
overlayController.hide();
}
BoxConstraints getTextFieldConstraints() {
double minHeight = DesktopAIPromptSizes.textFieldMinHeight +
DesktopAIPromptSizes.actionBarSendButtonSize +
DesktopAIChatSizes.inputActionBarMargin.vertical;
double maxHeight = 300;
if (showPredefinedFormatSection) {
minHeight += DesktopAIPromptSizes.predefinedFormatButtonHeight;
maxHeight += DesktopAIPromptSizes.predefinedFormatButtonHeight;
}
return BoxConstraints(minHeight: minHeight, maxHeight: maxHeight);
}
Widget inputTextField() {
return Shortcuts(
shortcuts: buildShortcuts(),
@@ -381,8 +356,7 @@ class _DesktopPromptInputState extends State<DesktopPromptInput> {
cubit: inputControlCubit,
textController: textController,
textFieldFocusNode: focusNode,
contentPadding:
calculateContentPadding(state.showPredefinedFormats),
contentPadding: calculateContentPadding(),
hintText: switch (state.aiType) {
AIType.appflowyAI => LocaleKeys.chat_inputMessageHint.tr(),
AIType.localAI => LocaleKeys.chat_inputLocalAIMessageHint.tr()
@@ -395,20 +369,8 @@ class _DesktopPromptInputState extends State<DesktopPromptInput> {
);
}
BoxConstraints getTextFieldConstraints(bool showPredefinedFormats) {
double minHeight = DesktopAIPromptSizes.textFieldMinHeight +
DesktopAIPromptSizes.actionBarSendButtonSize +
DesktopAIChatSizes.inputActionBarMargin.vertical;
double maxHeight = 300;
if (showPredefinedFormats) {
minHeight += DesktopAIPromptSizes.predefinedFormatButtonHeight;
maxHeight += DesktopAIPromptSizes.predefinedFormatButtonHeight;
}
return BoxConstraints(minHeight: minHeight, maxHeight: maxHeight);
}
EdgeInsetsGeometry calculateContentPadding(bool showPredefinedFormats) {
final top = showPredefinedFormats
EdgeInsetsGeometry calculateContentPadding() {
final top = showPredefinedFormatSection
? DesktopAIPromptSizes.predefinedFormatButtonHeight
: 0.0;
final bottom = DesktopAIPromptSizes.actionBarSendButtonSize +
@@ -489,80 +451,29 @@ class _FocusNextItemIntent extends Intent {
const _FocusNextItemIntent();
}
class PromptInputTextField extends StatelessWidget {
const PromptInputTextField({
super.key,
required this.cubit,
required this.textController,
required this.textFieldFocusNode,
required this.contentPadding,
this.hintText = "",
});
final ChatInputControlCubit cubit;
final TextEditingController textController;
final FocusNode textFieldFocusNode;
final EdgeInsetsGeometry contentPadding;
final String hintText;
@override
Widget build(BuildContext context) {
return ExtendedTextField(
controller: textController,
focusNode: textFieldFocusNode,
decoration: InputDecoration(
border: InputBorder.none,
enabledBorder: InputBorder.none,
focusedBorder: InputBorder.none,
contentPadding: contentPadding,
hintText: hintText,
hintStyle: inputHintTextStyle(context),
isCollapsed: true,
isDense: true,
),
keyboardType: TextInputType.multiline,
textCapitalization: TextCapitalization.sentences,
minLines: 1,
maxLines: null,
style: Theme.of(context).textTheme.bodyMedium,
specialTextSpanBuilder: PromptInputTextSpanBuilder(
inputControlCubit: cubit,
specialTextStyle: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.primary,
fontWeight: FontWeight.w600,
),
),
);
}
TextStyle? inputHintTextStyle(BuildContext context) {
return Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).isLightMode
? const Color(0xFFBDC2C8)
: const Color(0xFF3C3E51),
);
}
}
class _PromptBottomActions extends StatelessWidget {
const _PromptBottomActions({
required this.textController,
required this.overlayController,
required this.focusNode,
required this.sendButtonState,
required this.showPredefinedFormats,
required this.predefinedFormat,
required this.onTogglePredefinedFormatSection,
required this.onStartMention,
required this.showPredefinedFormats,
required this.onSendPressed,
required this.onStopStreaming,
required this.selectedSourcesNotifier,
required this.onUpdateSelectedSources,
});
final TextEditingController textController;
final OverlayPortalController overlayController;
final FocusNode focusNode;
final bool showPredefinedFormats;
final PredefinedFormat predefinedFormat;
final void Function() onTogglePredefinedFormatSection;
final void Function() onStartMention;
final SendButtonState sendButtonState;
final void Function() onSendPressed;
final void Function() onStopStreaming;
final ValueNotifier<List<String>> selectedSourcesNotifier;
final void Function(List<String>) onUpdateSelectedSources;
@override
@@ -609,6 +520,7 @@ class _PromptBottomActions extends StatelessWidget {
Widget _predefinedFormatButton() {
return PromptInputDesktopToggleFormatButton(
showFormatBar: showPredefinedFormats,
predefinedFormat: predefinedFormat,
onTap: onTogglePredefinedFormatSection,
);
}
@@ -616,7 +528,6 @@ class _PromptBottomActions extends StatelessWidget {
Widget _selectSourcesButton(BuildContext context) {
return PromptInputDesktopSelectSourcesButton(
onUpdateSelectedSources: onUpdateSelectedSources,
selectedSourcesNotifier: selectedSourcesNotifier,
);
}
@@ -624,7 +535,21 @@ class _PromptBottomActions extends StatelessWidget {
// return PromptInputMentionButton(
// iconSize: DesktopAIPromptSizes.actionBarIconSize,
// buttonSize: DesktopAIPromptSizes.actionBarButtonSize,
// onTap: onStartMention,
// onTap: () {
// if (overlayController.isShowing) {
// return;
// }
// if (!focusNode.hasFocus) {
// focusNode.requestFocus();
// }
// textController.text += '@';
// Future.delayed(Duration.zero, () {
// context
// .read<ChatInputControlCubit>()
// .startSearching(textController.value);
// overlayController.show();
// });
// },
// );
// }
Loaded 100 of 495 files, more files were not shown because too many files have changed in this diff. Show more