Compare commits

...
42 Commits
Author SHA1 Message Date
Morn fd12d3a0b0 chore: update CHANGELOG.md (#7397) 2025-02-18 16:48:33 +08:00
Lucas f25821e84d chore: update auto updater feed url (#7396) 2025-02-18 16:14:13 +08:00
Morn c0aa0e0509 fix: some launch review issues (#7379)
* fix: some launch review issues

* fix: some launch review issues

* fix: some launch review issues

* feat: add change button for icon uploader
2025-02-17 16:08:51 +08:00
Lucas 55fbb7522b feat: switch ai mode on mobile (#7391)
* fix: the default page name should be empty when creating

* feat: switch ai model on mobile
2025-02-17 16:08:38 +08:00
Li, John Gen 15b4d496fd chore: update translations (#7387)
* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦
2025-02-17 15:23:11 +08:00
Lucas ad54ad0614 fix: the default page name should be empty when creating (#7389) 2025-02-17 15:13:25 +08:00
Lucas b18bbd0e82 fix: auto updater issue on Windows (#7385) 2025-02-17 09:56:30 +08:00
Lucas e028e45e93 fix: lock page issues (#7380)
* fix: unable to click the swith to lock/unlock page

* fix: add divider above delete button on mobile

* fix: enable lock/unlock page by tapping the lock icon

* chore: update translations

* fix: hide cursor when the page is locked

* fix: the inline databaes still can be edited if the document is locked

* fix: disable auto update checker

* chore: change my account to account & app
2025-02-14 17:02:25 +08:00
Lucas 0e7ac85f90 chore: add ai images label (#7376) 2025-02-13 14:56:16 +08:00
Lucas 8bb2541862 chore: upgrade version (#7374) 2025-02-13 13:57:37 +08:00
Lucas 133e61befd fix: clear background color of header row & header column (#7373) 2025-02-13 13:21:02 +08:00
Morn 9e98680861 feat: support slash menu on mobile (#7368)
* feat: support slash menu on mobile

* feat: support at menu on mobile

* feat: support plus menu on mobile
2025-02-13 12:45:56 +08:00
Khor Shu Heng b75fd673cd chore: update collab version (#7372) 2025-02-13 12:45:45 +08:00
Lucas 4a7e20b3a5 fix: save image should not copy the image (#7370)
* fix: save image should not copy the image

* fix: unable to scroll the table in AI Chat on mobile
2025-02-12 17:35:59 +08:00
MornandLucas.Xu bbe746c564 feat: support upload svg as icon (#7270)
* feat: support upload svg as icon

* feat: support upload icon by pasting a link

* feat: delete remote images when remove custon icons

* chore: add testing for pasting image link as custon icon

---------

Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io>
2025-02-12 15:08:50 +08:00
Reagan lee 189faa4def chore: update translations (#7351)
* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦
2025-02-12 15:08:01 +08:00
Lucas c1a8d89938 feat: lock page on mobile (#7366)
* feat: support lock button in view more actions

* feat: add lock page on mobile

* feat: disable actions in locked page

* feat: disable more actions in locked page

* feat: support locked grid on mobile

* feat: support locked board/calendar on mobile

* fix: exclude lock page button from AI Chat
2025-02-12 15:07:21 +08:00
Lucas 71ce9affbe feat: lock page (#7353)
* feat: lock page

* feat: add pageLockStatus bloc

* feat: add lock status and unlock status in title bar

* feat: add loading lock status

* feat: disable moveTo, delete, rename, updateIcon operations if the page is locked

* fix: lock toast issue

* feat: support locked database

* feat: support locked grid

* feat: support locked title

* feat: support locked board

* feat: support locked calendar
2025-02-12 09:49:36 +08:00
Morn 552dba5abe fix: support exporting more content to markdown (#7333)
* fix: support exporting to markdown with multiple images

* fix: support exporting to markdown with database

* fix: support exporting to markdown with date or reminder

* fix: support exporting to markdown with subpage and page reference

* chore: add some testing for markdown parser

* chore: add testing for exporting markdown with databse as csv
2025-02-11 21:46:02 +08:00
Richard Shiue 04e3246976 chore: rename predefined format enum variant (#7359) 2025-02-11 16:40:39 +08:00
Morn 5d73c3d194 fix: gallery not rendering in row page (#7349) 2025-02-11 14:03:49 +08:00
Richard Shiue 12d9a98831 chore: disable impeller on android (#7355) 2025-02-11 14:03:29 +08:00
Lucas 8f646a2843 feat: integrate version checker for Linux (#7346)
* feat: integrate auto_updater in macOS

* chore: update translations

* chore: bump auto_updater version

* feat: exclude linux platform in auto update task

* feat: support auto_updater on Linux

* chore: combine version checker and auto updater into same class
2025-02-10 15:07:53 +08:00
Lucas f53e9d6549 feat: integrate auto_updater for macOS (#7328)
* feat: integrate auto_updater in macOS

* chore: update translations

* chore: bump auto_updater version

* feat: exclude linux platform in auto update task

* chore: disable auto updater

* fix: integration tests

* fix: integration tests
2025-02-10 09:20:24 +08:00
Richard Shiue fc9c152553 fix(flutter_desktop): selection in AI chat going missing while scrolling (#7281) 2025-02-07 18:44:55 +08:00
Lucas 00cdee831d chore: upgrade to Flutter 3.27.4 (#7230) 2025-02-07 18:17:46 +08:00
Morn 17ae05a623 fix: pasting a link on iOS results in incorrect behavior (#7326) 2025-02-07 14:49:50 +08:00
hasanbeder 8b1a03713b feat(i18n): Add Turkish (tr-TR) language translation (#7329)
- Complete Turkish language translation for AppFlowy
- Covers all UI elements and user-facing strings
- Improves localization support for Turkish users
2025-02-07 09:58:15 +08:00
Richard Shiue e4b57033b4 fix: improve calendar event button icon color and add confirm dialog (#7330)
* fix: improve calendar event button icon color and add confirm dialog

* test: update test
2025-02-06 22:46:22 +08:00
Richard ShiueandNathan 62a6fb8913 chore: optional response format (#7204)
* chore: optional response format

* chore: bump client api

* chore: code cleanup

* chore: bump client api

---------

Co-authored-by: Nathan <nathan@appflowy.io>
2025-02-06 18:10:23 +08:00
FakhriAzzouz 0d89e22ed2 chore(i18n): update ar-SA translations (#7312)
* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦
2025-02-06 13:54:16 +08:00
ArtemisOne ff2aae213c chore(i18n): update es-VE and ga-IE translations (#7320)
* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦

* chore: update translations with Fink 🐦
2025-02-06 13:53:24 +08:00
Richard Shiue 43e64d8219 test: fix image integration test (#7323) 2025-02-05 19:45:21 +08:00
Richard Shiue 6823fe5d24 fix: row document images (#7322)
* fix: row document images

* fix: calendar

* chore: fallback to documentbloc
2025-02-05 16:40:03 +08:00
Lucas f683085618 fix: unable to upload file on Android devices (#7314) 2025-02-04 20:57:06 +08:00
Nathan.fooo 71a22dc466 chore: fix ai page user profile refresh (#7317) 2025-02-04 20:29:56 +08:00
Lucas eb508a3ec9 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 14:05:57 +08:00
Nathan.fooo aacd09d8e2 chore: Support new error code (#7311)
* chore: fetch model list

* chore: suppor new error code
2025-02-03 20:52:08 +08:00
Peter Jose 25a27dfa81 chore(i18n): update id-ID translations (#7290)
- Translate 'themeMode' label
- Adjust text 'fontFamily'
- Update 'layoutDirection' translation
- Update 'textDirection' translation
2025-02-03 10:41:05 +08:00
Mohammad Mahdi Momeni 36349778e3 chore(i18n): update fa translations (#7292) 2025-02-03 10:05:34 +08:00
Nathan.fooo 9271d42db5 chore: fetch model list (#7306)
* chore: fetch model list
2025-02-01 23:48:32 +08:00
Richard Shiue 90d6e98b51 fix(flutter_mobile): drop focus on tap outside (#7274) 2025-01-24 09:23:52 +08:00
315 changed files with 8964 additions and 2396 deletions

No files matched your search

+1 -1
View File
@@ -18,7 +18,7 @@ on:
env:
CARGO_TERM_COLOR: always
FLUTTER_VERSION: "3.22.3"
FLUTTER_VERSION: "3.27.4"
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.22.2"
FLUTTER_VERSION: "3.27.4"
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.22.3"
FLUTTER_VERSION: "3.27.4"
RUST_TOOLCHAIN: "1.81.0"
concurrency:
+11 -11
View File
@@ -6,7 +6,7 @@ on:
- "*"
env:
FLUTTER_VERSION: "3.22.0"
FLUTTER_VERSION: "3.27.4"
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.22.0"
FLUTTER_VERSION: "3.27.4"
RUST_TOOLCHAIN: "1.81.0"
jobs:
+11
View File
@@ -1,4 +1,15 @@
# Release Notes
## 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
## Version 0.8.2 - 23/01/2025
### New Features
- Customized database view icons
+1 -1
View File
@@ -4,7 +4,7 @@ workflows:
instance_type: mac_mini_m2
max_build_duration: 30
environment:
flutter: 3.22.3
flutter: 3.27.4
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.2"
APPFLOWY_VERSION = "0.8.4"
FLUTTER_DESKTOP_FEATURES = "dart"
PRODUCT_NAME = "AppFlowy"
MACOSX_DEPLOYMENT_TARGET = "11.0"
@@ -43,6 +43,8 @@
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" />
@@ -0,0 +1,4 @@
<?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>

After

Width:  |  Height:  |  Size: 617 B

@@ -0,0 +1,12 @@
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
@@ -0,0 +1,26 @@
-----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
@@ -0,0 +1,36 @@
-----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-----
@@ -1,5 +1,6 @@
import 'data_migration/data_migration_test_runner.dart'
as data_migration_test_runner;
import 'database/database_test_runner.dart' as database_test_runner;
import 'document/document_test_runner.dart' as document_test_runner;
import 'set_env.dart' as preset_af_cloud_env_test;
import 'sidebar/sidebar_icon_test.dart' as sidebar_icon_test;
@@ -28,4 +29,7 @@ Future<void> main() async {
sidebar_move_page_test.main();
sidebar_rename_untitled_test.main();
sidebar_icon_test.main();
// database
database_test_runner.main();
}
@@ -31,12 +31,6 @@ 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
@@ -0,0 +1,80 @@
import 'dart:io';
import 'package:appflowy/core/config/kv.dart';
import 'package:appflowy/core/config/kv_keys.dart';
import 'package:appflowy/env/cloud_env.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/resizeable_image.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pbenum.dart';
import 'package:appflowy_editor/appflowy_editor.dart'
hide UploadImageMenu, ResizableImage;
import 'package:easy_localization/easy_localization.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/constants.dart';
import '../../../shared/database_test_op.dart';
import '../../../shared/mock/mock_file_picker.dart';
import '../../../shared/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
// copy link to block
group('database image:', () {
testWidgets('insert image', (tester) async {
await tester.initializeAppFlowy(
cloudType: AuthenticatorType.appflowyCloudSelfHost,
);
await tester.tapGoogleLoginInButton();
await tester.expectToSeeHomePageWithGetStartedPage();
// open the first row detail page and upload an image
await tester.createNewPageInSpace(
spaceName: Constants.generalSpaceName,
layout: ViewLayoutPB.Grid,
pageName: 'database image',
);
await tester.openFirstRowDetailPage();
// insert an image block
{
await tester.editor.tapLineOfEditorAt(0);
await tester.editor.showSlashMenu();
await tester.editor.tapSlashMenuItemWithName(
LocaleKeys.document_slashMenu_name_image.tr(),
);
}
// upload an image
{
final image = await rootBundle.load('assets/test/images/sample.jpeg');
final tempDirectory = await getTemporaryDirectory();
final imagePath = p.join(tempDirectory.path, 'sample.jpeg');
final file = File(imagePath)
..writeAsBytesSync(image.buffer.asUint8List());
mockPickFilePaths(
paths: [imagePath],
);
await getIt<KeyValueStorage>().set(KVKeys.kCloudType, '0');
await tester.tapButtonWithName(
LocaleKeys.document_imageBlock_upload_placeholder.tr(),
);
await tester.pumpAndSettle();
expect(find.byType(ResizableImage), findsOneWidget);
final node = tester.editor.getCurrentEditorState().getNodeAtPath([0])!;
expect(node.type, ImageBlockKeys.type);
expect(node.attributes[ImageBlockKeys.url], isNotEmpty);
// remove the temp file
file.deleteSync();
}
});
});
}
@@ -0,0 +1,9 @@
import 'package:integration_test/integration_test.dart';
import 'database_image_test.dart' as database_image_test;
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
database_image_test.main();
}
@@ -57,12 +57,6 @@ 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();
@@ -458,7 +458,7 @@ void main() {
});
testWidgets('paste the image url', (tester) async {
const plainText = 'https://appflowy.io/1.jpg';
const plainText = 'http://example.com/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),
@@ -1,5 +1,3 @@
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';
@@ -7,11 +5,9 @@ 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:flutter/services.dart';
import 'package:flowy_svg/flowy_svg.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';
@@ -215,17 +211,12 @@ void main() {
}
});
testWidgets('Update page custom icon in title bar', (tester) async {
testWidgets('Update page custom image icon in title bar', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
/// prepare local image
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);
final iconData = await tester.prepareImageIcon();
// create document, board, grid and calendar views
for (final value in ViewLayoutPB.values) {
@@ -259,4 +250,97 @@ 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,12 +1,16 @@
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();
@@ -18,7 +22,7 @@ void main() {
// mock the file picker
final path = await mockSaveFilePath(
p.join(context.applicationDataDirectory, 'test.md'),
p.join(context.applicationDataDirectory, 'test.zip'),
);
// click the share button and select markdown
await tester.tapShareButton();
@@ -28,10 +32,14 @@ void main() {
tester.expectToExportSuccess();
final file = File(path);
final isExist = file.existsSync();
expect(isExist, true);
final markdown = file.readAsStringSync();
expect(markdown, expectedMarkdown);
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);
}
}
});
testWidgets(
@@ -57,7 +65,7 @@ void main() {
final path = await mockSaveFilePath(
p.join(
context.applicationDataDirectory,
'${shareButtonState.view.name}.md',
'${shareButtonState.view.name}.zip',
),
);
@@ -69,10 +77,44 @@ void main() {
tester.expectToExportSuccess();
final file = File(path);
final isExist = file.existsSync();
expect(isExist, true);
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);
}
}
},
);
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);
});
});
}
@@ -0,0 +1,41 @@
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);
});
});
}
@@ -1,8 +1,11 @@
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;
@@ -13,6 +16,9 @@ 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,15 +1,12 @@
import 'dart:io';
import 'package:appflowy/generated/flowy_svgs.g.dart';
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/services.dart';
import 'package:flutter/material.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';
@@ -18,16 +15,11 @@ void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('document title:', () {
testWidgets('update page custom icon in title bar', (tester) async {
testWidgets('update page custom image icon in title bar', (tester) async {
await tester.launchInAnonymousMode();
/// prepare local image
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);
final iconData = await tester.prepareImageIcon();
/// create an empty page
await tester
@@ -50,16 +42,63 @@ void main() {
/// check result
final documentPage = find.byType(MobileDocumentScreen);
final rawEmojiIconWidget = find
final rawEmojiIconFinder = find
.descendant(
of: documentPage,
matching: find.byType(RawEmojiIconWidget),
)
.evaluate()
.first
.widget as RawEmojiIconWidget;
.last;
final rawEmojiIconWidget =
rawEmojiIconFinder.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,6 +1,9 @@
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';
@@ -85,5 +88,32 @@ 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);
});
});
}
@@ -0,0 +1,84 @@
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,6 +50,8 @@ 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';
@@ -677,6 +679,25 @@ 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),
@@ -935,6 +956,45 @@ 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,6 +1464,7 @@ extension AppFlowyDatabaseTest on WidgetTester {
);
await tapButton(button);
await tapButtonWithName(LocaleKeys.button_delete.tr());
}
Future<void> dragDropRescheduleCalendarEvent() async {
@@ -1,20 +1,24 @@
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 {
@@ -90,7 +94,7 @@ extension EmojiTestExtension on WidgetTester {
final dropTargetWidget = dropTarget.evaluate().first.widget as DropTarget;
dropTargetWidget.onDragDone?.call(
DropDoneDetails(
files: [XFile(icon.emoji)],
files: [DropItemFile(icon.emoji)],
localPosition: Offset.zero,
globalPosition: Offset.zero,
),
@@ -104,4 +108,37 @@ 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,6 +8,7 @@ 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';
@@ -252,16 +253,19 @@ 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: find.byType(FlowyNetworkImage),
matching: isSvg
? find.byType(FlowyNetworkSvg)
: find.byType(FlowyNetworkImage),
);
expect(image, findsOneWidget);
} else {
final image = find.descendant(
of: pageName,
matching: find.byType(Image),
matching: isSvg ? find.byType(SvgPicture) : find.byType(Image),
);
expect(image, findsOneWidget);
}
@@ -290,16 +294,26 @@ 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: find.byType(FlowyNetworkImage),
matching: isSvg
? find.byType(FlowyNetworkSvg)
: find.byType(FlowyNetworkImage),
);
expect(image, findsOneWidget);
} else {
final image = find.descendant(
of: find.byType(ViewTitleBar),
matching: find.byType(Image),
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),
);
expect(image, findsOneWidget);
}
+18 -11
View File
@@ -1,5 +1,5 @@
PODS:
- app_links (0.0.1):
- app_links (0.0.2):
- Flutter
- appflowy_backend (0.0.1):
- Flutter
@@ -66,6 +66,8 @@ 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)
@@ -79,7 +81,7 @@ PODS:
- shared_preferences_foundation (0.0.1):
- Flutter
- FlutterMacOS
- sqflite (0.0.3):
- sqflite_darwin (0.0.4):
- Flutter
- FlutterMacOS
- super_native_extensions (0.0.1):
@@ -90,6 +92,7 @@ PODS:
- Flutter
- webview_flutter_wkwebview (0.0.1):
- Flutter
- FlutterMacOS
DEPENDENCIES:
- app_links (from `.symlinks/plugins/app_links/ios`)
@@ -108,13 +111,14 @@ 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 (from `.symlinks/plugins/sqflite/darwin`)
- sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/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/ios`)
- webview_flutter_wkwebview (from `.symlinks/plugins/webview_flutter_wkwebview/darwin`)
SPEC REPOS:
trunk:
@@ -159,23 +163,25 @@ 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:
:path: ".symlinks/plugins/sqflite/darwin"
sqflite_darwin:
:path: ".symlinks/plugins/sqflite_darwin/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/ios"
:path: ".symlinks/plugins/webview_flutter_wkwebview/darwin"
SPEC CHECKSUMS:
app_links: c5161ac5ab5383ad046884568b4b91cb52df5d91
app_links: 3da4c36b46cac3bf24eb897f1a6ce80bda109874
appflowy_backend: 78f6a053f756e6bc29bcc5a2106cbe77b756e97a
connectivity_plus: 481668c94744c30c53b8895afb39159d1e619bdf
device_info_plus: 71ffc6ab7634ade6267c7a93088ed7e4f74e5896
@@ -186,7 +192,7 @@ SPEC CHECKSUMS:
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
fluttertoast: 76fea30fcf04176325f6864c87306927bd7d2038
image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a
integration_test: d5929033778cc4991a187e4e1a85396fa4f59b3a
integration_test: 4a889634ef21a45d28d50d622cf412dc6d9f586e
irondash_engine_context: 8e58ca8e0212ee9d1c7dc6a42121849986c88486
keyboard_height_plugin: ef70a8181b29f27670e9e2450814ca6b6dc05b05
open_filex: 432f3cd11432da3e39f47fcc0df2b1603854eff1
@@ -194,17 +200,18 @@ 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: c35dad70033b8862124f8337cc994a809fcd9fa3
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
super_native_extensions: b763c02dc3a8fd078389f410bf15149179020cb4
SwiftyGif: 6c3eafd0ce693cad58bb63d2b2fb9bacb8552780
Toast: 91b396c56ee72a5790816f40d3a94dd357abc196
url_launcher_ios: 694010445543906933d732453a59da0a173ae33d
webview_flutter_wkwebview: 45a041c7831641076618876de3ba75c712860c6b
webview_flutter_wkwebview: 44d4dee7d7056d5ad185d25b38404436d56c547c
PODFILE CHECKSUM: d0d9b4ff572d8695c38eb3f9b490f55cdfc57eca
@@ -1,7 +1,7 @@
import UIKit
import Flutter
@UIApplicationMain
@main
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
+73 -70
View File
@@ -1,75 +1,78 @@
<?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>NSAllowsArbitraryLoads</key>
<true/>
<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>NSAllowsArbitraryLoads</key>
<true />
</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 />
</dict>
<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>
</plist>
@@ -148,6 +148,15 @@ class CompletionStream {
);
}
if (event.startsWith("AI_MAX_REQUIRED:")) {
final msg = event.substring(16);
onError(
AIError(
message: msg,
),
);
}
if (event.startsWith("start:")) {
await onStart();
}
@@ -49,7 +49,9 @@ class _PromptInputMentionPageMenuState
void initState() {
super.initState();
Future.delayed(Duration.zero, () {
context.read<ChatInputControlCubit>().refreshViews();
if (mounted) {
context.read<ChatInputControlCubit>().refreshViews();
}
});
}
@@ -69,7 +69,7 @@ class ChangeFormatBar extends StatelessWidget {
_buildFormatButton(context, ImageFormat.image),
if (predefinedFormat?.imageFormat.hasText ?? true) ...[
_buildDivider(),
_buildTextFormatButton(context, TextFormat.auto),
_buildTextFormatButton(context, TextFormat.paragraph),
_buildTextFormatButton(context, TextFormat.bulletList),
_buildTextFormatButton(context, TextFormat.numberedList),
_buildTextFormatButton(context, TextFormat.table),
@@ -88,7 +88,8 @@ class ChangeFormatBar extends StatelessWidget {
return;
}
if (format.hasText) {
final textFormat = predefinedFormat?.textFormat ?? TextFormat.auto;
final textFormat =
predefinedFormat?.textFormat ?? TextFormat.paragraph;
onSelectPredefinedFormat(
PredefinedFormat(imageFormat: format, textFormat: textFormat),
);
+2 -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,
@@ -247,6 +247,7 @@ Future<AppFlowyCloudConfiguration> configurationFromUri(
// In development mode, the app is configured to access the AppFlowy cloud server directly through specific ports.
// This setup bypasses the need for Nginx, meaning that the AppFlowy cloud should be running without an Nginx server
// in the development environment.
// If you modify following code, please update the corresponding documentation in the appflowy billing.
if (authenticatorType == AuthenticatorType.appflowyCloudDevelop) {
return AppFlowyCloudConfiguration(
base_url: "$baseUrl:8000",
@@ -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.withOpacity(0.12).
// color will also change to foregroundColor.withValues(alpha: 0.12).
effectiveStyle = entry.enabled && i == focusedIndex
? effectiveStyle.copyWith(
backgroundColor: WidgetStatePropertyAll<Color>(
focusedBackgroundColor.withOpacity(0.12),
focusedBackgroundColor.withValues(alpha: 0.12),
),
)
: effectiveStyle;
@@ -1,3 +1,4 @@
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';
@@ -7,6 +8,7 @@ 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';
@@ -18,6 +20,9 @@ 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';
@@ -91,7 +96,7 @@ class _MobileViewPageState extends State<MobileViewPage> {
final body = _buildBody(context, state);
if (view == null) {
return _buildApp(context, null, body);
return SizedBox.shrink();
}
return MultiBlocProvider(
@@ -122,6 +127,11 @@ 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) {
@@ -152,6 +162,7 @@ class _MobileViewPageState extends State<MobileViewPage> {
title: title,
appBarOpacity: _appBarOpacity,
actions: actions,
view: view,
)
: FlowyAppBar(title: title, actions: actions);
final body = isDocument
@@ -222,6 +233,8 @@ 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) {
@@ -240,7 +253,7 @@ class _MobileViewPageState extends State<MobileViewPage> {
}
}
if (view.layout.isDocumentView) {
if (view.layout.isDocumentView && !isLocked) {
actions.addAll([
MobileViewPageLayoutButton(
view: view,
@@ -270,25 +283,139 @@ class _MobileViewPageState extends State<MobileViewPage> {
Widget _buildTitle(BuildContext context, ViewPB? view) {
final icon = view?.icon;
return Row(
mainAxisSize: MainAxisSize.min,
children: [
if (icon != null && icon.value.isNotEmpty) ...[
RawEmojiIconWidget(
emoji: icon.toEmojiIconData(),
emojiSize: 15,
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),
],
),
const HSpace(4),
],
Expanded(
child: FlowyText.medium(
widget.fixedTitle ?? view?.name ?? widget.title ?? '',
fontSize: 15.0,
overflow: TextOverflow.ellipsis,
figmaLineHeight: 18.0,
),
),
],
);
},
);
}
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_s,
color: const Color(0xFFD95A0B),
),
),
);
} 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();
},
);
}
@@ -12,6 +12,7 @@ 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';
@@ -28,12 +29,13 @@ 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;
@@ -43,9 +45,9 @@ class MobileViewPageImmersiveAppBar extends StatelessWidget
valueListenable: appBarOpacity,
builder: (_, opacity, __) => FlowyAppBar(
backgroundColor:
AppBarTheme.of(context).backgroundColor?.withOpacity(opacity),
AppBarTheme.of(context).backgroundColor?.withValues(alpha: opacity),
showDivider: false,
title: Opacity(opacity: opacity >= 0.99 ? 1.0 : 0, child: title),
title: _buildTitle(context, opacity: opacity),
leadingWidth: 44,
leading: Padding(
padding: const EdgeInsets.only(top: 4.0, bottom: 4.0, left: 12.0),
@@ -56,6 +58,13 @@ class MobileViewPageImmersiveAppBar extends StatelessWidget
);
}
Widget _buildTitle(
BuildContext context, {
required double opacity,
}) {
return title;
}
Widget _buildAppBarBackButton(BuildContext context) {
return AppBarButton(
padding: EdgeInsets.zero,
@@ -102,6 +111,12 @@ 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),
),
@@ -224,7 +239,7 @@ class _ImmersiveAppBarButton extends StatelessWidget {
child = DecoratedBox(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(dimension / 2.0),
color: Colors.black.withOpacity(0.2),
color: Colors.black.withValues(alpha: 0.2),
),
child: child,
);
@@ -14,6 +14,7 @@ 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';
@@ -43,7 +44,8 @@ class MobileViewPageMoreBottomSheet extends StatelessWidget {
},
child: ViewPageBottomSheet(
view: view,
onAction: (action) async => _onAction(context, action),
onAction: (action, {arguments}) async =>
_onAction(context, action, arguments),
onRename: (name) {
_onRename(context, name);
context.pop();
@@ -56,6 +58,7 @@ class MobileViewPageMoreBottomSheet extends StatelessWidget {
Future<void> _onAction(
BuildContext context,
MobileViewBottomSheetBodyAction action,
Map<String, dynamic>? arguments,
) async {
switch (action) {
case MobileViewBottomSheetBodyAction.duplicate:
@@ -107,12 +110,32 @@ 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,8 +1,10 @@
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,
@@ -40,6 +42,8 @@ class MobileViewItemBottomSheetBody extends StatelessWidget {
BuildContext context,
MobileViewItemBottomSheetBodyAction action,
) {
final isLocked =
context.read<ViewLockStatusBloc?>()?.state.isLocked ?? false;
switch (action) {
case MobileViewItemBottomSheetBodyAction.rename:
return FlowyOptionTile.text(
@@ -49,6 +53,7 @@ class MobileViewItemBottomSheetBody extends StatelessWidget {
FlowySvgs.view_item_rename_s,
size: Size.square(18),
),
enable: !isLocked,
showTopBorder: false,
showBottomBorder: false,
onTap: () => onAction(
@@ -94,6 +99,7 @@ class MobileViewItemBottomSheetBody extends StatelessWidget {
size: const Size.square(18),
color: Theme.of(context).colorScheme.error,
),
enable: !isLocked,
showTopBorder: false,
showBottomBorder: false,
onTap: () => onAction(
@@ -4,9 +4,12 @@ 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';
@@ -25,11 +28,27 @@ 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({
@@ -56,7 +75,7 @@ class _ViewPageBottomSheetState extends State<ViewPageBottomSheet> {
case MobileBottomSheetType.view:
return MobileViewBottomSheetBody(
view: widget.view,
onAction: (action) {
onAction: (action, {arguments}) {
switch (action) {
case MobileViewBottomSheetBodyAction.rename:
setState(() {
@@ -64,7 +83,7 @@ class _ViewPageBottomSheetState extends State<ViewPageBottomSheet> {
});
break;
default:
widget.onAction(action);
widget.onAction(action, arguments: arguments);
}
},
);
@@ -93,6 +112,8 @@ 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: [
@@ -100,6 +121,7 @@ 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,
),
@@ -118,6 +140,28 @@ 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,
@@ -138,12 +182,14 @@ 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,
),
@@ -206,3 +252,36 @@ 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,6 +8,7 @@ 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';
@@ -131,6 +132,11 @@ 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.withOpacity(0.3);
barrierColor ??= Colors.black.withValues(alpha: 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)
.withOpacity(secondaryAnimation.value * 0.1),
.withValues(alpha: secondaryAnimation.value * 0.1),
BlendMode.srcOver,
),
child: child,
@@ -1,5 +1,3 @@
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';
@@ -13,12 +11,14 @@ 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,6 +143,8 @@ 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
@@ -160,15 +162,20 @@ class _BoardContentState extends State<_BoardContent> {
padding: config.groupHeaderPadding,
)
: const HSpace(16),
trailing: showCreateGroupButton
trailing: showCreateGroupButton && !isLocked
? const MobileBoardTrailing()
: const HSpace(16),
headerBuilder: (_, groupData) => BlocProvider<BoardBloc>.value(
value: context.read<BoardBloc>(),
child: GroupCardHeader(
groupData: groupData,
),
),
headerBuilder: (_, groupData) {
final isLocked =
context.read<ViewLockStatusBloc?>()?.state.isLocked ??
false;
return IgnorePointer(
ignoring: isLocked,
child: GroupCardHeader(
groupData: groupData,
),
);
},
footerBuilder: _buildFooter,
cardBuilder: (_, column, columnItem) => _buildCard(
context: context,
@@ -184,34 +191,39 @@ 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: 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(
child: IgnorePointer(
ignoring: isLocked,
child: TextButton.icon(
style: TextButton.styleFrom(
padding: const EdgeInsets.only(left: 8),
alignment: Alignment.centerLeft,
),
icon: FlowySvg(
FlowySvgs.add_m,
color: style.colorScheme.onSurface,
),
),
onPressed: () => context.read<BoardBloc>().add(
BoardEvent.createRow(
columnData.id,
OrderObjectPositionTypePB.End,
null,
null,
),
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,
),
),
),
),
);
}
@@ -231,6 +243,8 @@ 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),
@@ -238,31 +252,34 @@ class _BoardContentState extends State<_BoardContent> {
decoration: _makeBoxDecoration(context),
child: BlocProvider.value(
value: boardBloc,
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,
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,
),
userProfile: boardBloc.userProfile,
),
),
);
@@ -276,14 +293,20 @@ class _BoardContentState extends State<_BoardContent> {
border: themeMode == ThemeMode.light
? Border.fromBorderSide(
BorderSide(
color: Theme.of(context).colorScheme.outline.withOpacity(0.5),
color: Theme.of(context)
.colorScheme
.outline
.withValues(alpha: 0.5),
),
)
: null,
boxShadow: themeMode == ThemeMode.light
? [
BoxShadow(
color: Theme.of(context).colorScheme.outline.withOpacity(0.5),
color: Theme.of(context)
.colorScheme
.outline
.withValues(alpha: 0.5),
blurRadius: 4,
offset: const Offset(0, 2),
),
@@ -1,5 +1,3 @@
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';
@@ -8,6 +6,7 @@ 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 {
@@ -49,7 +48,7 @@ class _MobileEditPropertyScreenState extends State<MobileEditPropertyScreen> {
final fieldId = widget.field.id;
return PopScope(
onPopInvoked: (didPop) {
onPopInvokedWithResult: (didPop, _) {
if (!didPop) {
context.pop(_fieldOptionValues);
}
@@ -75,7 +75,7 @@ class MobileDatabaseViewQuickActions extends StatelessWidget {
enableBackgroundColorSelection: false,
onSelectedEmoji: (r) {
ViewBackendService.updateViewIcon(
viewId: view.id,
view: view,
viewIcon: r.data,
);
Navigator.pop(context);
@@ -84,7 +84,11 @@ class MobileDatabaseViewQuickActions extends StatelessWidget {
);
},
builder: (_) => const SizedBox.shrink(),
).then((_) => Navigator.pop(context));
).then((_) {
if (context.mounted) {
Navigator.pop(context);
}
});
},
!isInline,
),
@@ -3,16 +3,19 @@ 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({
@@ -70,29 +73,47 @@ 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 showThirdPartyLogin =
final isLocalAuthEnabled =
userProfile.authenticator == AuthenticatorPB.Local && isAuthEnabled;
return SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
PersonalInfoSettingGroup(
userProfile: userProfile,
'';
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),
],
),
),
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.withOpacity(0.1),
tileColor: theme.colorScheme.onSurface.withValues(alpha: 0.1),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
@@ -134,7 +134,8 @@ 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.withOpacity(0.2),
color:
Theme.of(context).colorScheme.onSurfaceVariant.withValues(alpha: 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.withOpacity(0.45);
: Colors.white.withValues(alpha: 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: layout.defaultName,
name: '',
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.withOpacity(0.05),
color: Colors.black.withValues(alpha: 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.withOpacity(0.7)
: outlineColor.withOpacity(0.3);
? outlineColor.withValues(alpha: 0.7)
: outlineColor.withValues(alpha: 0.3);
return BoxDecoration(
borderRadius: BorderRadius.circular(30),
color: Theme.of(context).colorScheme.surface,
@@ -11,7 +11,6 @@ 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';
@@ -73,7 +72,14 @@ class _MobileSpaceTabState extends State<MobileSpaceTab>
listener: (context, state) {
final lastCreatedPage = state.lastCreatedPage;
if (lastCreatedPage != null) {
context.pushView(lastCreatedPage);
context.pushView(
lastCreatedPage,
tabs: [
PickerTabType.emoji,
PickerTabType.icon,
PickerTabType.custom,
].map((e) => e.name).toList(),
);
}
},
),
@@ -173,8 +179,6 @@ class _MobileSpaceTabState extends State<MobileSpaceTab>
);
case MobileSpaceTabType.favorites:
return MobileFavoriteSpace(userProfile: widget.userProfile);
default:
throw Exception('Unknown tab type: $tab');
}
}).toList();
}
@@ -188,7 +192,7 @@ class _MobileSpaceTabState extends State<MobileSpaceTab>
if (context.read<SpaceBloc>().state.spaces.isNotEmpty) {
context.read<SpaceBloc>().add(
SpaceEvent.createPage(
name: layout.defaultName,
name: '',
layout: layout,
openAfterCreate: true,
),
@@ -197,7 +201,7 @@ class _MobileSpaceTabState extends State<MobileSpaceTab>
// only support create document in section
context.read<SidebarSectionsBloc>().add(
SidebarSectionsEvent.createRootViewInSection(
name: layout.defaultName,
name: '',
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).withOpacity(0.12),
color: const Color(0x01717171).withValues(alpha: 0.12),
width: 0.8,
),
),
@@ -101,8 +101,6 @@ class WorkspaceMenuMoreOptions extends StatelessWidget {
WorkspaceMenuMoreOption.leave,
),
);
default:
return const Placeholder();
}
}
}
@@ -0,0 +1,253 @@
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;
}
}
@@ -0,0 +1,151 @@
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);
}
}
@@ -0,0 +1,152 @@
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.withOpacity(0.95)
: const Color(0xFF23262B).withOpacity(0.95);
? Colors.white.withValues(alpha: 0.95)
: const Color(0xFF23262B).withValues(alpha: 0.95);
}
Color get borderColor {
return Theme.of(this).isLightMode
? const Color(0x141F2329)
: const Color(0xFF23262B).withOpacity(0.5);
: const Color(0xFF23262B).withValues(alpha: 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).withOpacity(0.8);
return const Color(0xFFffffff).withValues(alpha: 0.8);
}
}
@@ -0,0 +1,220 @@
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,
);
}
}
}
@@ -0,0 +1,18 @@
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;
}
@@ -0,0 +1,113 @@
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 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),
);
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),
);
}
@@ -0,0 +1,361 @@
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;
}
}
@@ -0,0 +1,42 @@
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);
}
}
@@ -0,0 +1,106 @@
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,7 +83,6 @@ 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,6 +12,7 @@ class MobileQuickActionButton extends StatelessWidget {
this.iconColor,
this.iconSize,
this.enable = true,
this.rightIconBuilder,
});
final VoidCallback onTap;
@@ -21,34 +22,39 @@ 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 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,
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,
),
),
],
HSpace(30 - iconSize.width),
Expanded(
child: FlowyText.regular(
text,
fontSize: 16,
color: textColor,
),
),
if (rightIconBuilder != null) rightIconBuilder!(context),
],
),
),
),
);
@@ -37,6 +37,7 @@ class FlowyOptionTile extends StatelessWidget {
this.backgroundColor,
this.fontFamily,
this.height,
this.enable = true,
});
factory FlowyOptionTile.text({
@@ -49,6 +50,7 @@ class FlowyOptionTile extends StatelessWidget {
Widget? trailing,
VoidCallback? onTap,
double? height,
bool enable = true,
}) {
return FlowyOptionTile._(
type: FlowyOptionTileType.text,
@@ -61,6 +63,7 @@ class FlowyOptionTile extends StatelessWidget {
leading: leftIcon,
trailing: trailing,
height: height,
enable: enable,
);
}
@@ -77,6 +80,7 @@ class FlowyOptionTile extends StatelessWidget {
Widget? trailing,
String? textFieldHintText,
bool autofocus = false,
bool enable = true,
}) {
return FlowyOptionTile._(
type: FlowyOptionTileType.textField,
@@ -90,6 +94,7 @@ class FlowyOptionTile extends StatelessWidget {
onTextChanged: onTextChanged,
onTextSubmitted: onTextSubmitted,
autofocus: autofocus,
enable: enable,
);
}
@@ -105,6 +110,7 @@ class FlowyOptionTile extends StatelessWidget {
bool showBottomBorder = true,
String? fontFamily,
Color? backgroundColor,
bool enable = true,
}) {
return FlowyOptionTile._(
key: key,
@@ -119,6 +125,7 @@ class FlowyOptionTile extends StatelessWidget {
showTopBorder: showTopBorder,
showBottomBorder: showBottomBorder,
leading: leftIcon,
enable: enable,
trailing: isSelected
? const FlowySvg(
FlowySvgs.m_blue_check_s,
@@ -136,6 +143,7 @@ class FlowyOptionTile extends StatelessWidget {
bool showTopBorder = true,
bool showBottomBorder = true,
Widget? leftIcon,
bool enable = true,
}) {
return FlowyOptionTile._(
type: FlowyOptionTileType.toggle,
@@ -146,6 +154,7 @@ class FlowyOptionTile extends StatelessWidget {
showBottomBorder: showBottomBorder,
leading: leftIcon,
trailing: _Toggle(value: isSelected, onChanged: onValueChanged),
enable: enable,
);
}
@@ -181,11 +190,13 @@ class FlowyOptionTile extends StatelessWidget {
final double? height;
final bool enable;
@override
Widget build(BuildContext context) {
final leadingWidget = _buildLeading();
final child = FlowyOptionDecorateBox(
Widget child = FlowyOptionDecorateBox(
color: backgroundColor,
showTopBorder: showTopBorder,
showBottomBorder: showBottomBorder,
@@ -209,12 +220,21 @@ class FlowyOptionTile extends StatelessWidget {
if (type == FlowyOptionTileType.checkbox ||
type == FlowyOptionTileType.toggle ||
type == FlowyOptionTileType.text) {
return GestureDetector(
child = GestureDetector(
onTap: onTap,
child: child,
);
}
if (!enable) {
child = Opacity(
opacity: 0.5,
child: IgnorePointer(
child: child,
),
);
}
return child;
}
@@ -299,7 +319,7 @@ class _Toggle extends StatelessWidget {
fit: BoxFit.fill,
child: CupertinoSwitch(
value: value,
activeColor: Theme.of(context).colorScheme.primary,
activeTrackColor: Theme.of(context).colorScheme.primary,
onChanged: onChanged,
),
),
@@ -99,7 +99,7 @@ Future<T?> showFlowyCupertinoConfirmDialog<T>({
}) {
return showDialog(
context: context ?? AppGlobals.context,
barrierColor: Colors.black.withOpacity(0.25),
barrierColor: Colors.black.withValues(alpha: 0.25),
builder: (context) => CupertinoAlertDialog(
title: FlowyText.medium(
title,
@@ -105,6 +105,13 @@ class ChatAIMessageBloc extends Bloc<ChatAIMessageEvent, ChatAIMessageState> {
),
);
},
onAIMaxRequired: (message) {
emit(
state.copyWith(
messageState: MessageState.onAIMaxRequired(message),
),
);
},
receiveMetadata: (metadata) {
Log.debug("AI Steps: ${metadata.progress?.step}");
emit(
@@ -146,6 +153,12 @@ class ChatAIMessageBloc extends Bloc<ChatAIMessageEvent, ChatAIMessageState> {
add(ChatAIMessageEvent.receiveMetadata(metadata));
}
},
onAIMaxRequired: (message) {
if (!isClosed) {
Log.info(message);
add(ChatAIMessageEvent.onAIMaxRequired(message));
}
},
);
}
}
@@ -159,6 +172,8 @@ class ChatAIMessageEvent with _$ChatAIMessageEvent {
const factory ChatAIMessageEvent.onAIResponseLimit() = _OnAIResponseLimit;
const factory ChatAIMessageEvent.onAIImageResponseLimit() =
_OnAIImageResponseLimit;
const factory ChatAIMessageEvent.onAIMaxRequired(String message) =
_OnAIMaxRquired;
const factory ChatAIMessageEvent.receiveMetadata(
MetadataCollection metadata,
) = _ReceiveMetadata;
@@ -193,6 +208,7 @@ class MessageState with _$MessageState {
const factory MessageState.onError(String error) = _Error;
const factory MessageState.onAIResponseLimit() = _AIResponseLimit;
const factory MessageState.onAIImageResponseLimit() = _AIImageResponseLimit;
const factory MessageState.onAIMaxRequired(String message) = _AIMaxRequired;
const factory MessageState.ready() = _Ready;
const factory MessageState.loading() = _Loading;
}
@@ -149,7 +149,7 @@ class PredefinedFormat extends Equatable {
const PredefinedFormat.auto()
: imageFormat = ImageFormat.text,
textFormat = TextFormat.auto;
textFormat = TextFormat.paragraph;
final ImageFormat imageFormat;
final TextFormat? textFormat;
@@ -162,7 +162,7 @@ class PredefinedFormat extends Equatable {
ImageFormat.textAndImage => ResponseImageFormatPB.TextAndImage,
},
textFormat: switch (textFormat) {
TextFormat.auto => ResponseTextFormatPB.Paragraph,
TextFormat.paragraph => ResponseTextFormatPB.Paragraph,
TextFormat.bulletList => ResponseTextFormatPB.BulletedList,
TextFormat.numberedList => ResponseTextFormatPB.NumberedList,
TextFormat.table => ResponseTextFormatPB.Table,
@@ -201,14 +201,14 @@ enum ImageFormat {
}
enum TextFormat {
auto,
paragraph,
bulletList,
numberedList,
table;
FlowySvgData get icon {
return switch (this) {
TextFormat.auto => FlowySvgs.ai_paragraph_s,
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,
@@ -217,7 +217,7 @@ enum TextFormat {
String get i18n {
return switch (this) {
TextFormat.auto => LocaleKeys.chat_changeFormat_text.tr(),
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(),
@@ -28,6 +28,14 @@ class AnswerStream {
} else if (event == "AI_IMAGE_RESPONSE_LIMIT") {
_aiImageLimitReached = true;
_onAIImageResponseLimit?.call();
} else if (event.startsWith("AI_MAX_REQUIRED:")) {
final msg = event.substring(16);
// If the callback is not registered yet, add the event to the buffer.
if (_onAIMaxRequired != null) {
_onAIMaxRequired!(msg);
} else {
_pendingAIMaxRequiredEvents.add(msg);
}
}
},
onDone: () {
@@ -56,8 +64,12 @@ class AnswerStream {
void Function(String error)? _onError;
void Function()? _onAIResponseLimit;
void Function()? _onAIImageResponseLimit;
void Function(String message)? _onAIMaxRequired;
void Function(MetadataCollection metadataCollection)? _onMetadata;
// Buffer for events that occur before listen() is called.
final List<String> _pendingAIMaxRequiredEvents = [];
int get nativePort => _port.sendPort.nativePort;
bool get hasStarted => _hasStarted;
bool get aiLimitReached => _aiLimitReached;
@@ -78,6 +90,7 @@ class AnswerStream {
void Function(String error)? onError,
void Function()? onAIResponseLimit,
void Function()? onAIImageResponseLimit,
void Function(String message)? onAIMaxRequired,
void Function(MetadataCollection metadata)? onMetadata,
}) {
_onData = onData;
@@ -87,6 +100,15 @@ class AnswerStream {
_onAIResponseLimit = onAIResponseLimit;
_onAIImageResponseLimit = onAIImageResponseLimit;
_onMetadata = onMetadata;
_onAIMaxRequired = onAIMaxRequired;
// Flush any buffered AI_MAX_REQUIRED events.
if (_onAIMaxRequired != null && _pendingAIMaxRequiredEvents.isNotEmpty) {
for (final msg in _pendingAIMaxRequiredEvents) {
_onAIMaxRequired!(msg);
}
_pendingAIMaxRequiredEvents.clear();
}
_onStart?.call();
}
@@ -121,21 +121,24 @@ class _ChatContentPage extends StatelessWidget {
child: Align(
alignment: Alignment.topCenter,
child: _wrapConstraints(
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,
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,
),
),
),
),
@@ -7,10 +7,9 @@ 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,7 +75,8 @@ class ChatEditorStyleCustomizer extends EditorStyleCustomizer {
fontSize: fontSize,
fontWeight: FontWeight.normal,
color: Colors.red,
backgroundColor: theme.colorScheme.inverseSurface.withOpacity(0.8),
backgroundColor:
theme.colorScheme.inverseSurface.withValues(alpha: 0.8),
),
),
),
@@ -144,7 +145,7 @@ class ChatEditorStyleCustomizer extends EditorStyleCustomizer {
return TextStyle(
fontFamily: defaultFontFamily,
height: 1.5,
color: AFThemeExtension.of(context).onBackground.withOpacity(0.6),
color: AFThemeExtension.of(context).onBackground.withValues(alpha: 0.6),
);
}
}
@@ -138,16 +138,18 @@ class _MobileChatInputState extends State<MobileChatInput> {
else
const VSpace(8.0),
inputTextField(context),
Container(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Row(
children: [
const HSpace(8.0),
leadingButtons(context),
const Spacer(),
sendButton(),
const HSpace(12.0),
],
TextFieldTapRegion(
child: Container(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Row(
children: [
const HSpace(8.0),
leadingButtons(context),
const Spacer(),
sendButton(),
const HSpace(12.0),
],
),
),
),
],
@@ -281,6 +283,7 @@ class _MobileChatInputState extends State<MobileChatInput> {
fontWeight: FontWeight.w600,
),
),
onTapOutside: (_) => focusNode.unfocus(),
);
},
);
@@ -125,14 +125,14 @@ class WelcomeSampleQuestion extends StatelessWidget {
spreadRadius: -2,
color: isLightMode
? const Color(0x051F2329)
: Theme.of(context).shadowColor.withOpacity(0.02),
: Theme.of(context).shadowColor.withValues(alpha: 0.02),
),
BoxShadow(
offset: const Offset(0, 2),
blurRadius: 4,
color: isLightMode
? const Color(0x051F2329)
: Theme.of(context).shadowColor.withOpacity(0.02),
: Theme.of(context).shadowColor.withValues(alpha: 0.02),
),
BoxShadow(
offset: const Offset(0, 2),
@@ -140,7 +140,7 @@ class WelcomeSampleQuestion extends StatelessWidget {
spreadRadius: 2,
color: isLightMode
? const Color(0x051F2329)
: Theme.of(context).shadowColor.withOpacity(0.02),
: Theme.of(context).shadowColor.withValues(alpha: 0.02),
),
],
),
@@ -122,7 +122,7 @@ class _Body extends StatelessWidget {
opacity: predefinedFormat?.imageFormat.hasText ?? true ? 1 : 0,
child: Column(
children: [
_buildTextFormatButton(TextFormat.auto, true),
_buildTextFormatButton(TextFormat.paragraph, true),
_buildTextFormatButton(TextFormat.bulletList),
_buildTextFormatButton(TextFormat.numberedList),
_buildTextFormatButton(TextFormat.table),
@@ -153,7 +153,8 @@ class _Body extends StatelessWidget {
return;
}
if (format.hasText) {
final textFormat = predefinedFormat?.textFormat ?? TextFormat.auto;
final textFormat =
predefinedFormat?.textFormat ?? TextFormat.paragraph;
onSelectPredefinedFormat(
PredefinedFormat(imageFormat: format, textFormat: textFormat),
);
@@ -101,7 +101,7 @@ class _AppFlowyEditorMarkdownState extends State<_AppFlowyEditorMarkdown> {
styleCustomizer: styleCustomizer,
// the editor is not editable in the chat
editable: false,
alwaysDistributeSimpleTableColumnWidths: true,
alwaysDistributeSimpleTableColumnWidths: UniversalPlatform.isDesktop,
);
return IntrinsicHeight(
child: AppFlowyEditor(
@@ -109,6 +109,7 @@ class _AppFlowyEditorMarkdownState extends State<_AppFlowyEditorMarkdown> {
// the editor is not editable in the chat
editable: false,
disableKeyboardService: UniversalPlatform.isMobile,
disableSelectionService: UniversalPlatform.isMobile,
editorStyle: editorStyle,
editorScrollController: scrollController,
blockComponentBuilders: blockBuilders,
@@ -89,14 +89,14 @@ class _AIMessageActionBarState extends State<AIMessageActionBar> {
spreadRadius: -2,
color: isLightMode
? const Color(0x051F2329)
: Theme.of(context).shadowColor.withOpacity(0.02),
: Theme.of(context).shadowColor.withValues(alpha: 0.02),
),
BoxShadow(
offset: const Offset(0, 2),
blurRadius: 4,
color: isLightMode
? const Color(0x051F2329)
: Theme.of(context).shadowColor.withOpacity(0.02),
: Theme.of(context).shadowColor.withValues(alpha: 0.02),
),
BoxShadow(
offset: const Offset(0, 2),
@@ -104,7 +104,7 @@ class _AIMessageActionBarState extends State<AIMessageActionBar> {
spreadRadius: 2,
color: isLightMode
? const Color(0x051F2329)
: Theme.of(context).shadowColor.withOpacity(0.02),
: Theme.of(context).shadowColor.withValues(alpha: 0.02),
),
],
),
@@ -340,14 +340,14 @@ class _ChangeFormatPopoverContentState
spreadRadius: -2,
color: isLightMode
? const Color(0x051F2329)
: Theme.of(context).shadowColor.withOpacity(0.02),
: Theme.of(context).shadowColor.withValues(alpha: 0.02),
),
BoxShadow(
offset: const Offset(0, 2),
blurRadius: 4,
color: isLightMode
? const Color(0x051F2329)
: Theme.of(context).shadowColor.withOpacity(0.02),
: Theme.of(context).shadowColor.withValues(alpha: 0.02),
),
BoxShadow(
offset: const Offset(0, 2),
@@ -355,7 +355,7 @@ class _ChangeFormatPopoverContentState
spreadRadius: 2,
color: isLightMode
? const Color(0x051F2329)
: Theme.of(context).shadowColor.withOpacity(0.02),
: Theme.of(context).shadowColor.withValues(alpha: 0.02),
),
],
),
@@ -377,8 +377,9 @@ class _ChangeFormatPopoverContentState
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
widget.onRegenerate
?.call(predefinedFormat ?? const PredefinedFormat.auto());
if (predefinedFormat != null) {
widget.onRegenerate?.call(predefinedFormat!);
}
},
child: SizedBox.square(
dimension: DesktopAIPromptSizes.predefinedFormatButtonHeight,
@@ -10,7 +10,6 @@ import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_chat_core/flutter_chat_core.dart';
import 'package:universal_platform/universal_platform.dart';
import '../layout_define.dart';
import 'ai_markdown_text.dart';
@@ -106,10 +105,7 @@ class ChatAIMessageWidget extends StatelessWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
IgnorePointer(
ignoring: UniversalPlatform.isMobile,
child: AIMarkdownText(markdown: state.text),
),
AIMarkdownText(markdown: state.text),
if (state.sources.isNotEmpty)
AIMessageMetadata(
sources: state.sources,
@@ -137,6 +133,11 @@ class ChatAIMessageWidget extends StatelessWidget {
errorMessage: LocaleKeys.sideBar_purchaseAIMax.tr(),
);
},
onAIMaxRequired: (message) {
return ChatErrorMessageWidget(
errorMessage: message,
);
},
),
),
);
@@ -83,7 +83,6 @@ class TextMessageText extends StatelessWidget {
text,
lineHeight: 1.4,
maxLines: null,
selectable: true,
color: AFThemeExtension.of(context).textColor,
);
}
@@ -41,21 +41,21 @@ class CustomScrollToBottom extends StatelessWidget {
spreadRadius: 8,
color: isLightMode
? const Color(0x0F1F2329)
: Theme.of(context).shadowColor.withOpacity(0.06),
: Theme.of(context).shadowColor.withValues(alpha: 0.06),
),
BoxShadow(
offset: const Offset(0, 4),
blurRadius: 8,
color: isLightMode
? const Color(0x141F2329)
: Theme.of(context).shadowColor.withOpacity(0.08),
: Theme.of(context).shadowColor.withValues(alpha: 0.08),
),
BoxShadow(
offset: const Offset(0, 2),
blurRadius: 4,
color: isLightMode
? const Color(0x1F1F2329)
: Theme.of(context).shadowColor.withOpacity(0.12),
: Theme.of(context).shadowColor.withValues(alpha: 0.12),
),
],
),
@@ -1,9 +1,5 @@
import 'dart:io';
import 'package:appflowy/util/field_type_extension.dart';
import 'package:flutter/material.dart' hide Card;
import 'package:flutter/services.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/database/board/mobile_board_page.dart';
@@ -19,6 +15,9 @@ import 'package:appflowy/plugins/database/widgets/cell/card_cell_style_maps/desk
import 'package:appflowy/plugins/database/widgets/row/row_detail.dart';
import 'package:appflowy/shared/conditional_listenable_builder.dart';
import 'package:appflowy/shared/flowy_error_page.dart';
import 'package:appflowy/util/field_type_extension.dart';
import 'package:appflowy/workspace/application/user/user_workspace_bloc.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/view.pb.dart';
import 'package:appflowy_board/appflowy_board.dart';
@@ -26,13 +25,14 @@ 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:flowy_infra_ui/style_widget/hover.dart';
import 'package:flutter/material.dart' hide Card;
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:universal_platform/universal_platform.dart';
import '../../widgets/card/card.dart';
import '../../widgets/cell/card_cell_builder.dart';
import '../application/board_bloc.dart';
import 'toolbar/board_setting_bar.dart';
import 'widgets/board_focus_scope.dart';
import 'widgets/board_hidden_groups.dart';
@@ -206,6 +206,7 @@ class _DesktopBoardPageState extends State<DesktopBoardPage> {
onEditStateChanged: widget.onEditStateChanged,
focusScope: _focusScope,
boardController: _boardController,
view: widget.view,
),
),
),
@@ -240,6 +241,7 @@ class _BoardContent extends StatefulWidget {
const _BoardContent({
required this.boardController,
required this.focusScope,
required this.view,
this.onEditStateChanged,
this.shrinkWrap = false,
});
@@ -248,6 +250,7 @@ class _BoardContent extends StatefulWidget {
final BoardFocusScope focusScope;
final VoidCallback? onEditStateChanged;
final bool shrinkWrap;
final ViewPB view;
@override
State<_BoardContent> createState() => _BoardContentState();
@@ -367,7 +370,7 @@ class _BoardContentState extends State<_BoardContent> {
scrollManager: scrollManager,
),
),
cardBuilder: (_, column, columnItem) => MultiBlocProvider(
cardBuilder: (context, column, columnItem) => MultiBlocProvider(
key: ValueKey("board_card_${column.id}_${columnItem.id}"),
providers: [
BlocProvider<BoardBloc>.value(
@@ -376,13 +379,24 @@ class _BoardContentState extends State<_BoardContent> {
BlocProvider.value(
value: context.read<BoardActionsCubit>(),
),
BlocProvider(
create: (_) => ViewLockStatusBloc(view: widget.view)
..add(ViewLockStatusEvent.initial()),
),
],
child: _BoardCard(
afGroupData: column,
groupItem: columnItem as GroupItem,
boardConfig: config,
notifier: widget.focusScope,
cellBuilder: cellBuilder,
child: BlocBuilder<ViewLockStatusBloc, ViewLockStatusState>(
builder: (context, state) {
return IgnorePointer(
ignoring: state.isLocked,
child: _BoardCard(
afGroupData: column,
groupItem: columnItem as GroupItem,
boardConfig: config,
notifier: widget.focusScope,
cellBuilder: cellBuilder,
),
);
},
),
),
),
@@ -714,19 +728,19 @@ class _BoardCardState extends State<_BoardCard> {
.isFocused(GroupedRowId(rowId: rowId, groupId: groupId))
? Theme.of(context).colorScheme.primary
: Theme.of(context).brightness == Brightness.light
? const Color(0xFF1F2329).withOpacity(0.12)
? const Color(0xFF1F2329).withValues(alpha: 0.12)
: const Color(0xFF59647A),
),
),
boxShadow: [
BoxShadow(
blurRadius: 4,
color: const Color(0xFF1F2329).withOpacity(0.02),
color: const Color(0xFF1F2329).withValues(alpha: 0.02),
),
BoxShadow(
blurRadius: 4,
spreadRadius: -2,
color: const Color(0xFF1F2329).withOpacity(0.02),
color: const Color(0xFF1F2329).withValues(alpha: 0.02),
),
],
);
@@ -856,10 +870,13 @@ void _openCard({
FlowyOverlay.show(
context: context,
builder: (_) => RowDetailPage(
databaseController: databaseController,
rowController: rowController,
userProfile: context.read<BoardBloc>().userProfile,
builder: (_) => BlocProvider.value(
value: context.read<UserWorkspaceBloc>(),
child: RowDetailPage(
databaseController: databaseController,
rowController: rowController,
userProfile: context.read<BoardBloc>().userProfile,
),
),
);
}
@@ -1,5 +1,6 @@
import 'dart:io';
import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart';
import 'package:flutter/material.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
@@ -426,10 +427,13 @@ class HiddenGroupPopupItemList extends StatelessWidget {
onPressed: () {
FlowyOverlay.show(
context: context,
builder: (_) => RowDetailPage(
databaseController: databaseController,
rowController: rowController,
userProfile: context.read<BoardBloc>().userProfile,
builder: (_) => BlocProvider.value(
value: context.read<UserWorkspaceBloc>(),
child: RowDetailPage(
databaseController: databaseController,
rowController: rowController,
userProfile: context.read<BoardBloc>().userProfile,
),
),
);
PopoverContainer.of(context).close();
@@ -256,16 +256,16 @@ class NewEventButton extends StatelessWidget {
boxShadow: [
BoxShadow(
spreadRadius: -2,
color: const Color(0xFF1F2329).withOpacity(0.02),
color: const Color(0xFF1F2329).withValues(alpha: 0.02),
blurRadius: 2,
),
BoxShadow(
color: const Color(0xFF1F2329).withOpacity(0.02),
color: const Color(0xFF1F2329).withValues(alpha: 0.02),
blurRadius: 4,
),
BoxShadow(
spreadRadius: 2,
color: const Color(0xFF1F2329).withOpacity(0.02),
color: const Color(0xFF1F2329).withValues(alpha: 0.02),
blurRadius: 8,
),
],
@@ -1,24 +1,23 @@
import 'package:appflowy/plugins/database/application/row/row_controller.dart';
import 'package:appflowy/plugins/database/widgets/row/row_detail.dart';
import 'package:flutter/material.dart';
import 'package:appflowy/mobile/presentation/database/card/card_detail/mobile_card_detail_screen.dart';
import 'package:appflowy/plugins/database/application/database_controller.dart';
import 'package:appflowy/plugins/database/application/row/row_cache.dart';
import 'package:appflowy/plugins/database/application/row/row_controller.dart';
import 'package:appflowy/plugins/database/widgets/card/card.dart';
import 'package:appflowy/plugins/database/widgets/cell/card_cell_builder.dart';
import 'package:appflowy/plugins/database/widgets/cell/card_cell_style_maps/calendar_card_cell_style.dart';
import 'package:appflowy/plugins/database/widgets/row/row_detail.dart';
import 'package:appflowy/workspace/application/view/view_bloc.dart';
import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart';
import 'package:flowy_infra/size.dart';
import 'package:flowy_infra/theme_extension.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:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
import 'package:universal_platform/universal_platform.dart';
import '../application/calendar_bloc.dart';
import 'calendar_event_editor.dart';
class EventCard extends StatefulWidget {
@@ -128,16 +127,16 @@ class _EventCardState extends State<EventCard> {
boxShadow: [
BoxShadow(
spreadRadius: -2,
color: const Color(0xFF1F2329).withOpacity(0.02),
color: const Color(0xFF1F2329).withValues(alpha: 0.02),
blurRadius: 2,
),
BoxShadow(
color: const Color(0xFF1F2329).withOpacity(0.02),
color: const Color(0xFF1F2329).withValues(alpha: 0.02),
blurRadius: 4,
),
BoxShadow(
spreadRadius: 2,
color: const Color(0xFF1F2329).withOpacity(0.02),
color: const Color(0xFF1F2329).withValues(alpha: 0.02),
blurRadius: 8,
),
],
@@ -178,10 +177,13 @@ class _EventCardState extends State<EventCard> {
FlowyOverlay.show(
context: context,
builder: (_) => RowDetailPage(
databaseController: widget.databaseController,
rowController: rowController,
userProfile: context.read<CalendarBloc>().userProfile,
builder: (_) => BlocProvider.value(
value: context.read<UserWorkspaceBloc>(),
child: RowDetailPage(
databaseController: widget.databaseController,
rowController: rowController,
userProfile: context.read<CalendarBloc>().userProfile,
),
),
);
},
@@ -12,6 +12,7 @@ import 'package:appflowy/plugins/database/widgets/cell/editable_cell_builder.dar
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/text.dart';
import 'package:appflowy/plugins/database/widgets/row/accessory/cell_accessory.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:collection/collection.dart';
import 'package:easy_localization/easy_localization.dart';
@@ -92,36 +93,55 @@ class EventEditorControls extends StatelessWidget {
message: LocaleKeys.calendar_duplicateEvent.tr(),
child: FlowyIconButton(
width: 20,
icon: const FlowySvg(
icon: FlowySvg(
FlowySvgs.m_duplicate_s,
size: Size.square(17),
size: const Size.square(16),
color: Theme.of(context).iconTheme.color,
),
iconColorOnHover: Theme.of(context).colorScheme.onSecondary,
onPressed: () => context.read<CalendarBloc>().add(
CalendarEvent.duplicateEvent(
rowController.viewId,
rowController.rowId,
),
),
onPressed: () {
context.read<CalendarBloc>().add(
CalendarEvent.duplicateEvent(
rowController.viewId,
rowController.rowId,
),
);
PopoverContainer.of(context).close();
},
),
),
const HSpace(8.0),
FlowyIconButton(
width: 20,
icon: const FlowySvg(FlowySvgs.delete_s),
iconColorOnHover: Theme.of(context).colorScheme.onSecondary,
onPressed: () => context.read<CalendarBloc>().add(
CalendarEvent.deleteEvent(
rowController.viewId,
rowController.rowId,
),
),
icon: FlowySvg(
FlowySvgs.delete_s,
size: const Size.square(16),
color: Theme.of(context).iconTheme.color,
),
onPressed: () {
showConfirmDeletionDialog(
context: context,
name: LocaleKeys.grid_row_label.tr(),
description: LocaleKeys.grid_row_deleteRowPrompt.tr(),
onConfirm: () {
context.read<CalendarBloc>().add(
CalendarEvent.deleteEvent(
rowController.viewId,
rowController.rowId,
),
);
PopoverContainer.of(context).close();
},
);
},
),
const HSpace(8.0),
FlowyIconButton(
width: 20,
icon: const FlowySvg(FlowySvgs.full_view_s),
iconColorOnHover: Theme.of(context).colorScheme.onSecondary,
icon: FlowySvg(
FlowySvgs.full_view_s,
size: const Size.square(16),
color: Theme.of(context).iconTheme.color,
),
onPressed: () {
PopoverContainer.of(context).close();
onExpand.call();
@@ -1,8 +1,3 @@
import 'package:appflowy/plugins/database/grid/presentation/grid_page.dart';
import 'package:appflowy/plugins/database/tab_bar/desktop/setting_menu.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
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/bottom_sheet/bottom_sheet.dart';
@@ -11,22 +6,26 @@ import 'package:appflowy/mobile/presentation/presentation.dart';
import 'package:appflowy/plugins/database/application/database_controller.dart';
import 'package:appflowy/plugins/database/calendar/application/calendar_bloc.dart';
import 'package:appflowy/plugins/database/calendar/application/unschedule_event_bloc.dart';
import 'package:appflowy/plugins/database/grid/presentation/grid_page.dart';
import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';
import 'package:appflowy/plugins/database/tab_bar/desktop/setting_menu.dart';
import 'package:appflowy/plugins/database/tab_bar/tab_bar_view.dart';
import 'package:appflowy/workspace/application/view/view_bloc.dart';
import 'package:appflowy/workspace/application/user/user_workspace_bloc.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/view.pb.dart';
import 'package:calendar_view/calendar_view.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/size.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';
import 'package:universal_platform/universal_platform.dart';
import '../../application/row/row_controller.dart';
import '../../widgets/row/row_detail.dart';
import 'calendar_day.dart';
import 'layout/sizes.dart';
import 'toolbar/calendar_setting_bar.dart';
@@ -123,8 +122,18 @@ class _CalendarPageState extends State<CalendarPage> {
Widget build(BuildContext context) {
return CalendarControllerProvider(
controller: _eventController,
child: BlocProvider<CalendarBloc>.value(
value: _calendarBloc,
child: MultiBlocProvider(
providers: [
BlocProvider<CalendarBloc>.value(
value: _calendarBloc,
),
BlocProvider(
create: (context) => ViewLockStatusBloc(view: widget.view)
..add(
ViewLockStatusEvent.initial(),
),
),
],
child: MultiBlocListener(
listeners: [
BlocListener<CalendarBloc, CalendarState>(
@@ -235,7 +244,21 @@ class _CalendarPageState extends State<CalendarPage> {
showBorder: false,
headerBuilder: _headerNavigatorBuilder,
weekDayBuilder: _headerWeekDayBuilder,
cellBuilder: _calendarDayBuilder,
cellBuilder: (
date,
calenderEvents,
isToday,
isInMonth,
position,
) =>
_calendarDayBuilder(
context,
date,
calenderEvents,
isToday,
isInMonth,
position,
),
useAvailableVerticalSpace: widget.shrinkWrap,
),
),
@@ -344,6 +367,7 @@ class _CalendarPageState extends State<CalendarPage> {
}
Widget _calendarDayBuilder(
BuildContext context,
DateTime date,
List<CalendarEventData<CalendarDayEvent>> calenderEvents,
isToday,
@@ -355,17 +379,22 @@ class _CalendarPageState extends State<CalendarPage> {
// is implemnted in the develop branch(WIP). Will be replaced with that.
final events = calenderEvents.map((value) => value.event!).toList()
..sort((a, b) => a.event.timestamp.compareTo(b.event.timestamp));
final isLocked =
context.watch<ViewLockStatusBloc?>()?.state.isLocked ?? false;
return CalendarDayCard(
viewId: widget.view.id,
isToday: isToday,
isInMonth: isInMonth,
events: events,
date: date,
rowCache: _calendarBloc.rowCache,
onCreateEvent: (date) =>
_calendarBloc.add(CalendarEvent.createEvent(date)),
position: position,
return IgnorePointer(
ignoring: isLocked,
child: CalendarDayCard(
viewId: widget.view.id,
isToday: isToday,
isInMonth: isInMonth,
events: events,
date: date,
rowCache: _calendarBloc.rowCache,
onCreateEvent: (date) =>
_calendarBloc.add(CalendarEvent.createEvent(date)),
position: position,
),
);
}
@@ -390,7 +419,7 @@ void showEventDetails({
context: context,
builder: (BuildContext overlayContext) {
return BlocProvider.value(
value: context.read<ViewBloc>(),
value: context.read<UserWorkspaceBloc>(),
child: RowDetailPage(
rowController: rowController,
databaseController: databaseController,
@@ -457,14 +486,18 @@ class _UnscheduledEventsButtonState extends State<UnscheduledEventsButton> {
),
),
),
popupBuilder: (_) => BlocProvider.value(
value: context.read<CalendarBloc>(),
child: BlocProvider.value(
value: context.read<ViewBloc>(),
child: UnscheduleEventsList(
databaseController: widget.databaseController,
unscheduleEvents: state.unscheduleEvents,
popupBuilder: (_) => MultiBlocProvider(
providers: [
BlocProvider.value(
value: context.read<CalendarBloc>(),
),
BlocProvider.value(
value: context.read<UserWorkspaceBloc>(),
),
],
child: UnscheduleEventsList(
databaseController: widget.databaseController,
unscheduleEvents: state.unscheduleEvents,
),
),
);
@@ -1,19 +1,18 @@
import 'dart:async';
import 'package:appflowy/plugins/database/grid/presentation/widgets/toolbar/grid_setting_bar.dart';
import 'package:appflowy/plugins/database/tab_bar/desktop/setting_menu.dart';
import 'package:flutter/material.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database/application/row/row_service.dart';
import 'package:appflowy/plugins/database/application/tab_bar_bloc.dart';
import 'package:appflowy/plugins/database/domain/sort_service.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/calculations/calculations_row.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/toolbar/grid_setting_bar.dart';
import 'package:appflowy/plugins/database/tab_bar/desktop/setting_menu.dart';
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_builder.dart';
import 'package:appflowy/shared/flowy_error_page.dart';
import 'package:appflowy/workspace/application/action_navigation/action_navigation_bloc.dart';
import 'package:appflowy/workspace/application/action_navigation/navigation_action.dart';
import 'package:appflowy/workspace/application/view/view_bloc.dart';
import 'package:appflowy/workspace/application/user/user_workspace_bloc.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-folder/view.pb.dart';
@@ -21,6 +20,7 @@ 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:flowy_infra_ui/style_widget/scrolling/styled_scrollview.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:linked_scroll_controller/linked_scroll_controller.dart';
import 'package:provider/provider.dart';
@@ -30,7 +30,6 @@ import '../../application/row/row_controller.dart';
import '../../tab_bar/tab_bar_view.dart';
import '../../widgets/row/row_detail.dart';
import '../application/grid_bloc.dart';
import 'grid_scroll.dart';
import 'layout/layout.dart';
import 'layout/sizes.dart';
@@ -140,8 +139,16 @@ class _GridPageState extends State<GridPage> {
@override
Widget build(BuildContext context) {
return BlocProvider<GridBloc>(
create: (_) => gridBloc,
return MultiBlocProvider(
providers: [
BlocProvider<GridBloc>(
create: (_) => gridBloc,
),
BlocProvider(
create: (context) => ViewLockStatusBloc(view: widget.view)
..add(ViewLockStatusEvent.initial()),
),
],
child: BlocListener<ActionNavigationBloc, ActionNavigationState>(
listener: (context, state) {
final action = state.action;
@@ -196,7 +203,7 @@ class _GridPageState extends State<GridPage> {
FlowyOverlay.show(
context: context,
builder: (_) => BlocProvider.value(
value: context.read<ViewBloc>(),
value: context.read<UserWorkspaceBloc>(),
child: RowDetailPage(
databaseController: context.read<GridBloc>().databaseController,
rowController: rowController,
@@ -233,7 +240,7 @@ class _GridPageState extends State<GridPage> {
FlowyOverlay.show(
context: context,
builder: (_) => BlocProvider.value(
value: context.read<ViewBloc>(),
value: context.read<UserWorkspaceBloc>(),
child: RowDetailPage(
databaseController:
context.read<GridBloc>().databaseController,
@@ -288,7 +295,10 @@ class _GridPageContentState extends State<GridPageContent> {
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
_GridHeader(headerScrollController: headerScrollController),
_GridHeader(
headerScrollController: headerScrollController,
editable: !context.read<ViewLockStatusBloc>().state.isLocked,
),
_GridRows(
viewId: widget.view.id,
scrollController: _scrollController,
@@ -300,18 +310,30 @@ class _GridPageContentState extends State<GridPageContent> {
}
class _GridHeader extends StatelessWidget {
const _GridHeader({required this.headerScrollController});
const _GridHeader({
required this.headerScrollController,
required this.editable,
});
final ScrollController headerScrollController;
final bool editable;
@override
Widget build(BuildContext context) {
return BlocBuilder<GridBloc, GridState>(
Widget child = BlocBuilder<GridBloc, GridState>(
builder: (_, state) => GridHeaderSliverAdaptor(
viewId: state.viewId,
anchorScrollController: headerScrollController,
),
);
if (!editable) {
child = IgnorePointer(
child: child,
);
}
return child;
}
}
@@ -474,7 +496,7 @@ class _GridRowsState extends State<_GridRows> {
proxyDecorator: (child, _, __) => Provider.value(
value: context.read<DatabasePluginWidgetBuilderSize>(),
child: Material(
color: Colors.white.withOpacity(.1),
color: Colors.white.withValues(alpha: .1),
child: Opacity(opacity: .5, child: child),
),
),
@@ -504,12 +526,21 @@ class _GridRowsState extends State<_GridRows> {
itemCount: itemCount,
itemBuilder: (context, index) {
if (index == itemCount - 1) {
return Column(
final child = Column(
key: const Key('grid_footer'),
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: footer,
);
if (context.read<ViewLockStatusBloc>().state.isLocked) {
return IgnorePointer(
key: const Key('grid_footer'),
child: child,
);
}
return child;
}
return _renderRow(
@@ -544,6 +575,7 @@ class _GridRowsState extends State<_GridRows> {
rowId: rowId,
viewId: viewId,
index: index,
editable: !context.watch<ViewLockStatusBloc>().state.isLocked,
rowController: RowController(
viewId: viewId,
rowMeta: rowMeta,
@@ -559,7 +591,7 @@ class _GridRowsState extends State<_GridRows> {
}
return BlocProvider.value(
value: context.read<ViewBloc>(),
value: context.read<UserWorkspaceBloc>(),
child: RowDetailPage(
rowController: RowController(
viewId: viewId,
@@ -9,6 +9,7 @@ import 'package:appflowy/plugins/database/tab_bar/tab_bar_view.dart';
import 'package:appflowy/shared/flowy_error_page.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/workspace/application/action_navigation/action_navigation_bloc.dart';
import 'package:appflowy/workspace/application/view/view_lock_status_bloc.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
@@ -182,6 +183,8 @@ class _GridPageContentState extends State<GridPageContent> {
@override
Widget build(BuildContext context) {
final isLocked =
context.read<ViewLockStatusBloc?>()?.state.isLocked ?? false;
return BlocListener<GridBloc, GridState>(
listenWhen: (previous, current) =>
previous.createdRow != current.createdRow,
@@ -215,7 +218,7 @@ class _GridPageContentState extends State<GridPageContent> {
),
],
),
if (!widget.shrinkWrap)
if (!widget.shrinkWrap && !isLocked)
Positioned(
bottom: 16,
right: 16,
@@ -356,7 +359,7 @@ class _GridRows extends StatelessWidget {
final databaseController = context.read<GridBloc>().databaseController;
final child = MobileGridRow(
Widget child = MobileGridRow(
key: ValueKey(rowMeta.id),
rowId: rowId,
isDraggable: isDraggable,
@@ -373,12 +376,20 @@ class _GridRows extends StatelessWidget {
);
if (animation != null) {
return SizeTransition(
child = SizeTransition(
sizeFactor: animation,
child: child,
);
}
final isLocked =
context.read<ViewLockStatusBloc?>()?.state.isLocked ?? false;
if (isLocked) {
child = IgnorePointer(
child: child,
);
}
return child;
}
}
@@ -1,5 +1,3 @@
import 'package:flutter/material.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database/grid/application/grid_bloc.dart';
@@ -9,6 +7,7 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/style_widget/button.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class GridAddRowButton extends StatelessWidget {
@@ -17,8 +16,8 @@ class GridAddRowButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
final color = Theme.of(context).brightness == Brightness.light
? const Color(0xFF171717).withOpacity(0.4)
: const Color(0xFFFFFFFF).withOpacity(0.4);
? const Color(0xFF171717).withValues(alpha: 0.4)
: const Color(0xFFFFFFFF).withValues(alpha: 0.4);
return FlowyButton(
radius: BorderRadius.zero,
decoration: BoxDecoration(
@@ -66,8 +65,8 @@ class GridRowLoadMoreButton extends StatelessWidget {
final padding =
context.read<DatabasePluginWidgetBuilderSize>().horizontalPadding;
final color = Theme.of(context).brightness == Brightness.light
? const Color(0xFF171717).withOpacity(0.4)
: const Color(0xFFFFFFFF).withOpacity(0.4);
? const Color(0xFF171717).withValues(alpha: 0.4)
: const Color(0xFFFFFFFF).withValues(alpha: 0.4);
return Container(
padding: GridSize.footerContentInsets.copyWith(left: 0) +
@@ -1,17 +1,16 @@
import 'package:appflowy/shared/icon_emoji_picker/icon_picker.dart';
import 'package:appflowy/util/theme_extension.dart';
import 'package:flutter/material.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/plugins/database/application/field/field_cell_bloc.dart';
import 'package:appflowy/plugins/database/application/field/field_controller.dart';
import 'package:appflowy/plugins/database/application/field/field_info.dart';
import 'package:appflowy/plugins/database/widgets/field/field_editor.dart';
import 'package:appflowy/shared/icon_emoji_picker/icon_picker.dart';
import 'package:appflowy/util/field_type_extension.dart';
import 'package:appflowy/util/theme_extension.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
import 'package:flowy_infra/theme_extension.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:flutter_bloc/flutter_bloc.dart';
import '../../layout/sizes.dart';
@@ -261,7 +260,7 @@ class FieldIcon extends StatelessWidget {
return svgContent == null
? FlowySvg(
fieldInfo.fieldType.svgData,
color: color.withOpacity(0.6),
color: color.withValues(alpha: 0.6),
size: Size.square(dimension),
)
: SizedBox.square(
@@ -269,7 +268,7 @@ class FieldIcon extends StatelessWidget {
child: Center(
child: FlowySvg.string(
svgContent,
color: color.withOpacity(0.45),
color: color.withValues(alpha: 0.45),
size: Size.square(dimension - 2),
),
),
@@ -5,6 +5,7 @@ import 'package:appflowy/plugins/database/application/field/field_controller.dar
import 'package:appflowy/plugins/database/application/field/field_info.dart';
import 'package:appflowy/plugins/database/grid/application/grid_bloc.dart';
import 'package:appflowy/plugins/database/grid/application/grid_header_bloc.dart';
import 'package:appflowy/workspace/application/view/view_lock_status_bloc.dart';
import 'package:collection/collection.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/theme_extension.dart';
@@ -39,6 +40,8 @@ class _MobileGridHeaderState extends State<MobileGridHeader> {
Widget build(BuildContext context) {
final fieldController =
context.read<GridBloc>().databaseController.fieldController;
final isLocked =
context.read<ViewLockStatusBloc?>()?.state.isLocked ?? false;
return BlocProvider(
create: (context) {
return GridHeaderBloc(
@@ -76,12 +79,15 @@ class _MobileGridHeaderState extends State<MobileGridHeader> {
);
},
),
SizedBox(
height: _kGridHeaderHeight,
child: _GridHeader(
viewId: widget.viewId,
fieldController: fieldController,
scrollController: widget.reorderableController,
IgnorePointer(
ignoring: isLocked,
child: SizedBox(
height: _kGridHeaderHeight,
child: _GridHeader(
viewId: widget.viewId,
fieldController: fieldController,
scrollController: widget.reorderableController,
),
),
),
],
@@ -1,27 +1,25 @@
import 'package:appflowy/plugins/database/domain/sort_service.dart';
import 'package:appflowy/plugins/database/grid/application/grid_bloc.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:flutter/material.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import "package:appflowy/generated/locale_keys.g.dart";
import 'package:appflowy/plugins/database/application/cell/cell_controller.dart';
import 'package:appflowy/plugins/database/application/field/field_controller.dart';
import 'package:appflowy/plugins/database/application/row/row_controller.dart';
import 'package:appflowy/plugins/database/application/row/row_service.dart';
import 'package:appflowy/plugins/database/domain/sort_service.dart';
import 'package:appflowy/plugins/database/grid/application/grid_bloc.dart';
import 'package:appflowy/plugins/database/grid/application/row/row_bloc.dart';
import 'package:appflowy/plugins/database/tab_bar/tab_bar_view.dart';
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_builder.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.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:provider/provider.dart';
import '../../../../widgets/row/accessory/cell_accessory.dart';
import '../../../../widgets/row/cells/cell_container.dart';
import '../../layout/sizes.dart';
import 'action.dart';
class GridRow extends StatelessWidget {
@@ -35,6 +33,7 @@ class GridRow extends StatelessWidget {
required this.openDetailPage,
required this.index,
this.shrinkWrap = false,
required this.editable,
});
final FieldController fieldController;
@@ -45,6 +44,7 @@ class GridRow extends StatelessWidget {
final void Function(BuildContext context) openDetailPage;
final int index;
final bool shrinkWrap;
final bool editable;
@override
Widget build(BuildContext context) {
@@ -58,7 +58,7 @@ class GridRow extends StatelessWidget {
rowContent = Expanded(child: rowContent);
}
return BlocProvider(
rowContent = BlocProvider(
create: (_) => RowBloc(
fieldController: fieldController,
rowId: rowId,
@@ -74,6 +74,14 @@ class GridRow extends StatelessWidget {
),
),
);
if (!editable) {
rowContent = IgnorePointer(
child: rowContent,
);
}
return rowContent;
}
}
@@ -263,7 +263,7 @@ class _TabBarItemButtonState extends State<TabBarItemButton> {
enableBackgroundColorSelection: false,
onSelectedEmoji: (r) {
ViewBackendService.updateViewIcon(
viewId: widget.view.id,
view: widget.view,
viewIcon: r.data,
);
if (!r.keepOpen) {
Loaded 100 of 315 files, more files were not shown because too many files have changed in this diff. Show more