Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fd12d3a0b0 | ||
|
|
f25821e84d | ||
|
|
c0aa0e0509 | ||
|
|
55fbb7522b | ||
|
|
15b4d496fd | ||
|
|
ad54ad0614 | ||
|
|
b18bbd0e82 | ||
|
|
e028e45e93 | ||
|
|
0e7ac85f90 | ||
|
|
8bb2541862 | ||
|
|
133e61befd | ||
|
|
9e98680861 | ||
|
|
b75fd673cd | ||
|
|
4a7e20b3a5 | ||
|
|
bbe746c564 | ||
|
|
189faa4def | ||
|
|
c1a8d89938 | ||
|
|
71ce9affbe | ||
|
|
552dba5abe | ||
|
|
04e3246976 | ||
|
|
5d73c3d194 | ||
|
|
12d9a98831 | ||
|
|
8f646a2843 | ||
|
|
f53e9d6549 | ||
|
|
fc9c152553 | ||
|
|
00cdee831d | ||
|
|
17ae05a623 | ||
|
|
8b1a03713b | ||
|
|
e4b57033b4 | ||
|
|
62a6fb8913 | ||
|
|
0d89e22ed2 | ||
|
|
ff2aae213c | ||
|
|
43e64d8219 | ||
|
|
6823fe5d24 | ||
|
|
f683085618 | ||
|
|
71a22dc466 | ||
|
|
eb508a3ec9 | ||
|
|
aacd09d8e2 | ||
|
|
25a27dfa81 | ||
|
|
36349778e3 | ||
|
|
9271d42db5 | ||
|
|
90d6e98b51 |
No files matched your search
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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-----
|
||||
@@ -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();
|
||||
}
|
||||
-6
@@ -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
|
||||
|
||||
+80
@@ -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();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
+9
@@ -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();
|
||||
}
|
||||
-6
@@ -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();
|
||||
|
||||
+1
-1
@@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
+50
-8
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+3
-2
@@ -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
@@ -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();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+19
-4
@@ -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,
|
||||
);
|
||||
|
||||
+24
-1
@@ -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;
|
||||
|
||||
+6
@@ -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(
|
||||
|
||||
+82
-3
@@ -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,
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+6
@@ -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) {
|
||||
|
||||
+1
-1
@@ -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,
|
||||
|
||||
+1
-1
@@ -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,
|
||||
|
||||
+78
-55
@@ -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),
|
||||
),
|
||||
|
||||
+2
-3
@@ -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);
|
||||
}
|
||||
|
||||
+6
-2
@@ -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,
|
||||
),
|
||||
|
||||
+42
-21
@@ -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),
|
||||
),
|
||||
|
||||
+2
-1
@@ -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,
|
||||
),
|
||||
|
||||
+1
-1
@@ -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,
|
||||
),
|
||||
),
|
||||
|
||||
-2
@@ -101,8 +101,6 @@ class WorkspaceMenuMoreOptions extends StatelessWidget {
|
||||
WorkspaceMenuMoreOption.leave,
|
||||
),
|
||||
);
|
||||
default:
|
||||
return const Placeholder();
|
||||
}
|
||||
}
|
||||
}
|
||||
+253
@@ -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;
|
||||
}
|
||||
}
|
||||
+151
@@ -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);
|
||||
}
|
||||
}
|
||||
+152
@@ -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);
|
||||
}
|
||||
}
|
||||
+220
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
+18
@@ -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;
|
||||
}
|
||||
+113
@@ -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),
|
||||
);
|
||||
}
|
||||
+361
@@ -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;
|
||||
}
|
||||
}
|
||||
+42
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
+29
-23
@@ -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,
|
||||
),
|
||||
),
|
||||
|
||||
+1
-1
@@ -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),
|
||||
);
|
||||
}
|
||||
}
|
||||
+13
-10
@@ -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),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
+3
-2
@@ -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),
|
||||
);
|
||||
|
||||
+2
-1
@@ -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,
|
||||
|
||||
+9
-8
@@ -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,
|
||||
|
||||
+6
-5
@@ -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,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
-1
@@ -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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
+8
-4
@@ -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();
|
||||
|
||||
+3
-3
@@ -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,
|
||||
),
|
||||
],
|
||||
|
||||
+14
-12
@@ -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,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
|
||||
+39
-19
@@ -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();
|
||||
|
||||
+61
-28
@@ -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,
|
||||
|
||||
+14
-3
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
+5
-6
@@ -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) +
|
||||
|
||||
+5
-6
@@ -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),
|
||||
),
|
||||
),
|
||||
|
||||
+12
-6
@@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
+15
-7
@@ -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
Reference in new issue
Block a user