Compare commits
8
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2d5946e75c | ||
|
|
f28a299786 | ||
|
|
4b0d61b534 | ||
|
|
cf8fb19976 | ||
|
|
1525a363f2 | ||
|
|
5a99da9a45 | ||
|
|
495226fa2c | ||
|
|
7ffd459875 |
No files matched your search
@@ -65,7 +65,7 @@ runs:
|
||||
sudo wget -qO /etc/apt/trusted.gpg.d/dart_linux_signing_key.asc https://dl-ssl.google.com/linux/linux_signing_key.pub
|
||||
sudo wget -qO /etc/apt/sources.list.d/dart_stable.list https://storage.googleapis.com/download.dartlang.org/linux/debian/dart_stable.list
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y dart curl build-essential libssl-dev clang cmake ninja-build pkg-config libgtk-3-dev keybinder-3.0 libnotify-dev libcurl4-openssl-dev
|
||||
sudo apt-get install -y dart curl build-essential libssl-dev clang cmake ninja-build pkg-config libgtk-3-dev keybinder-3.0 libnotify-dev
|
||||
;;
|
||||
Windows)
|
||||
vcpkg integrate install
|
||||
|
||||
@@ -52,7 +52,7 @@ runs:
|
||||
sudo wget -qO /etc/apt/trusted.gpg.d/dart_linux_signing_key.asc https://dl-ssl.google.com/linux/linux_signing_key.pub
|
||||
sudo wget -qO /etc/apt/sources.list.d/dart_stable.list https://storage.googleapis.com/download.dartlang.org/linux/debian/dart_stable.list
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y dart curl build-essential libssl-dev clang cmake ninja-build pkg-config libgtk-3-dev keybinder-3.0 libnotify-dev network-manager libcurl4-openssl-dev
|
||||
sudo apt-get install -y dart curl build-essential libssl-dev clang cmake ninja-build pkg-config libgtk-3-dev keybinder-3.0 libnotify-dev network-manager
|
||||
shell: bash
|
||||
|
||||
- name: Enable Flutter Desktop
|
||||
|
||||
@@ -19,7 +19,7 @@ on:
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
FLUTTER_VERSION: "3.27.4"
|
||||
RUST_TOOLCHAIN: "1.84.0"
|
||||
RUST_TOOLCHAIN: "1.81.0"
|
||||
CARGO_MAKE_VERSION: "0.37.18"
|
||||
CLOUD_VERSION: 0.6.54-amd64
|
||||
|
||||
|
||||
@@ -26,9 +26,9 @@ on:
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
FLUTTER_VERSION: "3.27.4"
|
||||
RUST_TOOLCHAIN: "1.84.0"
|
||||
RUST_TOOLCHAIN: "1.81.0"
|
||||
CARGO_MAKE_VERSION: "0.37.18"
|
||||
CLOUD_VERSION: 0.9.49-amd64
|
||||
CLOUD_VERSION: 0.6.54-amd64
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
@@ -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
|
||||
@@ -174,7 +174,7 @@ jobs:
|
||||
sudo wget -qO /etc/apt/trusted.gpg.d/dart_linux_signing_key.asc https://dl-ssl.google.com/linux/linux_signing_key.pub
|
||||
sudo wget -qO /etc/apt/sources.list.d/dart_stable.list https://storage.googleapis.com/download.dartlang.org/linux/debian/dart_stable.list
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y dart curl build-essential libssl-dev clang cmake ninja-build pkg-config libgtk-3-dev keybinder-3.0 libnotify-dev libcurl4-openssl-dev
|
||||
sudo apt-get install -y dart curl build-essential libssl-dev clang cmake ninja-build pkg-config libgtk-3-dev keybinder-3.0 libnotify-dev
|
||||
fi
|
||||
shell: bash
|
||||
|
||||
@@ -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
|
||||
@@ -308,7 +308,7 @@ jobs:
|
||||
sudo wget -qO /etc/apt/trusted.gpg.d/dart_linux_signing_key.asc https://dl-ssl.google.com/linux/linux_signing_key.pub
|
||||
sudo wget -qO /etc/apt/sources.list.d/dart_stable.list https://storage.googleapis.com/download.dartlang.org/linux/debian/dart_stable.list
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y dart curl build-essential libssl-dev clang cmake ninja-build pkg-config libgtk-3-dev keybinder-3.0 libnotify-dev libcurl4-openssl-dev
|
||||
sudo apt-get install -y dart curl build-essential libssl-dev clang cmake ninja-build pkg-config libgtk-3-dev keybinder-3.0 libnotify-dev
|
||||
shell: bash
|
||||
|
||||
- name: Enable Flutter Desktop
|
||||
@@ -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"
|
||||
|
||||
@@ -19,14 +19,35 @@ on:
|
||||
|
||||
env:
|
||||
FLUTTER_VERSION: "3.27.4"
|
||||
RUST_TOOLCHAIN: "1.84.0"
|
||||
RUST_TOOLCHAIN: "1.81.0"
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build-self-hosted:
|
||||
if: github.event.pull_request.head.repo.full_name == github.repository
|
||||
runs-on: self-hosted
|
||||
|
||||
steps:
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Build AppFlowy
|
||||
working-directory: frontend
|
||||
run: |
|
||||
cargo make --profile development-ios-arm64-sim appflowy-core-dev-ios
|
||||
cargo make --profile development-ios-arm64-sim code_generation
|
||||
|
||||
- uses: futureware-tech/simulator-action@v3
|
||||
id: simulator-action
|
||||
with:
|
||||
model: "iPhone 15"
|
||||
shutdown_after_job: false
|
||||
|
||||
integration-tests:
|
||||
if: github.event.pull_request.head.repo.full_name != github.repository
|
||||
runs-on: macos-latest
|
||||
|
||||
steps:
|
||||
@@ -73,26 +94,26 @@ jobs:
|
||||
cargo make --profile development-ios-arm64-sim appflowy-core-dev-ios
|
||||
cargo make --profile development-ios-arm64-sim code_generation
|
||||
|
||||
# - uses: futureware-tech/simulator-action@v3
|
||||
# id: simulator-action
|
||||
# with:
|
||||
# model: "iPhone 15"
|
||||
# shutdown_after_job: false
|
||||
- uses: futureware-tech/simulator-action@v3
|
||||
id: simulator-action
|
||||
with:
|
||||
model: "iPhone 15"
|
||||
shutdown_after_job: false
|
||||
|
||||
# - name: Run AppFlowy on simulator
|
||||
# working-directory: frontend/appflowy_flutter
|
||||
# run: |
|
||||
# flutter run -d ${{ steps.simulator-action.outputs.udid }} &
|
||||
# pid=$!
|
||||
# sleep 500
|
||||
# kill $pid
|
||||
# continue-on-error: true
|
||||
- name: Run AppFlowy on simulator
|
||||
working-directory: frontend/appflowy_flutter
|
||||
run: |
|
||||
flutter run -d ${{ steps.simulator-action.outputs.udid }} &
|
||||
pid=$!
|
||||
sleep 500
|
||||
kill $pid
|
||||
continue-on-error: true
|
||||
|
||||
# # Integration tests
|
||||
# - name: Run integration tests
|
||||
# working-directory: frontend/appflowy_flutter
|
||||
# # The integration tests are flaky and sometimes fail with "Connection timed out":
|
||||
# # Don't block the CI. If the tests fail, the CI will still pass.
|
||||
# # Instead, we're using Code Magic to re-run the tests to check if they pass.
|
||||
# continue-on-error: true
|
||||
# run: flutter test integration_test/runner.dart -d ${{ steps.simulator-action.outputs.udid }}
|
||||
# Integration tests
|
||||
- name: Run integration tests
|
||||
working-directory: frontend/appflowy_flutter
|
||||
# The integration tests are flaky and sometimes fail with "Connection timed out":
|
||||
# Don't block the CI. If the tests fail, the CI will still pass.
|
||||
# Instead, we're using Code Magic to re-run the tests to check if they pass.
|
||||
continue-on-error: true
|
||||
run: flutter test integration_test/runner.dart -d ${{ steps.simulator-action.outputs.udid }}
|
||||
@@ -7,7 +7,7 @@ on:
|
||||
|
||||
env:
|
||||
FLUTTER_VERSION: "3.27.4"
|
||||
RUST_TOOLCHAIN: "1.84.0"
|
||||
RUST_TOOLCHAIN: "1.81.0"
|
||||
|
||||
jobs:
|
||||
create-release:
|
||||
@@ -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-22.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
|
||||
@@ -366,10 +366,9 @@ jobs:
|
||||
run: |
|
||||
sudo wget -qO /etc/apt/trusted.gpg.d/dart_linux_signing_key.asc https://dl-ssl.google.com/linux/linux_signing_key.pub
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y build-essential libsqlite3-dev libssl-dev clang cmake ninja-build pkg-config libgtk-3-dev libcurl4-openssl-dev
|
||||
sudo apt-get install -y build-essential libsqlite3-dev libssl-dev clang cmake ninja-build pkg-config libgtk-3-dev
|
||||
sudo apt-get install keybinder-3.0
|
||||
sudo apt-get install -y alien libnotify-dev
|
||||
sudo apt install libfuse2
|
||||
source $HOME/.cargo/env
|
||||
cargo install --force --locked cargo-make
|
||||
cargo install --force --locked duckscript_cli
|
||||
|
||||
@@ -18,8 +18,8 @@ on:
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
CLOUD_VERSION: 0.9.49-amd64
|
||||
RUST_TOOLCHAIN: "1.84.0"
|
||||
CLOUD_VERSION: 0.8.3-amd64
|
||||
RUST_TOOLCHAIN: "1.81.0"
|
||||
|
||||
jobs:
|
||||
ubuntu-job:
|
||||
@@ -106,7 +106,7 @@ jobs:
|
||||
af_cloud_test_ws_url: ws://localhost/ws/v1
|
||||
af_cloud_test_gotrue_url: http://localhost/gotrue
|
||||
run: |
|
||||
DISABLE_CI_TEST_LOG="true" cargo test --no-default-features --features="dart" -- --skip local_ollama_test
|
||||
DISABLE_CI_TEST_LOG="true" cargo test --no-default-features --features="dart"
|
||||
|
||||
- name: rustfmt rust-lib
|
||||
run: cargo fmt --all -- --check
|
||||
|
||||
@@ -11,7 +11,7 @@ on:
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
FLUTTER_VERSION: "3.27.4"
|
||||
RUST_TOOLCHAIN: "1.84.0"
|
||||
RUST_TOOLCHAIN: "1.81.0"
|
||||
|
||||
jobs:
|
||||
tests:
|
||||
|
||||
@@ -1,67 +1,4 @@
|
||||
# Release Notes
|
||||
## Version 0.9.1 - 01/05/2025
|
||||
### Desktop
|
||||
#### New Features
|
||||
- Added AppFlowy Prompt Library to AI Chat and Document's Ask AI
|
||||
- Revamped the desktop in-app notification center
|
||||
- Supported login with password, as well as forgot and change password options
|
||||
- Supported copying link to invite members
|
||||
- Improved the Settings' Members tab with new metadata: member avatar and joined time
|
||||
#### Bug Fixes
|
||||
- Fixed data loss when using anonymous local
|
||||
- Fixed crash when trying to delete an emoji
|
||||
- Fixed Windows scaling issue
|
||||
- Correctly displayed mention text by decoding web content
|
||||
### Mobile
|
||||
#### New Features
|
||||
- Supported workspace search
|
||||
- Improved UX for links in documents
|
||||
- Supported changing password in Mobile Settings
|
||||
- Added support for inviting members via links
|
||||
#### Bug Fixes
|
||||
- Correctly displayed mention text by decoding web content
|
||||
|
||||
## Version 0.9.0 - 30/04/2025
|
||||
### Desktop
|
||||
#### New Features
|
||||
- Added AppFlowy Prompt Library to AI Chat and Document's Ask AI
|
||||
- Revamped the desktop in-app notification center
|
||||
- Supported login with password, as well as forgot and change password options
|
||||
- Supported copying link to invite members
|
||||
- Improved the Settings' Members tab with new metadata: member avatar and joined time
|
||||
#### Bug Fixes
|
||||
- Fixed crash when trying to delete an emoji
|
||||
- Fixed Windows scaling issue
|
||||
- Correctly displayed mention text by decoding web content
|
||||
### Mobile
|
||||
#### New Features
|
||||
- Supported workspace search
|
||||
- Improved UX for links in documents
|
||||
- Supported changing password in Mobile Settings
|
||||
- Added support for inviting members via links
|
||||
#### Bug Fixes
|
||||
- Correctly displayed mention text by decoding web content
|
||||
|
||||
## Version 0.8.9 - 16/04/2025
|
||||
### Desktop
|
||||
#### New Features
|
||||
- Supported pasting a link as a mention, providing a more condensed visualization of linked content
|
||||
- Supported converting between link formats (e.g. transforming a mention into a bookmark)
|
||||
- Improved the link editing experience with enhanced UX
|
||||
- Added OTP (One-Time Password) support for sign-in authentication
|
||||
- Added latest AI models: GPT-4.1, GPT-4.1-mini, and Claude 3.7 Sonnet
|
||||
#### Bug Fixes
|
||||
- Fixed an issue where properties were not displaying in the row detail page
|
||||
- Fixed a bug where Undo didn't work in the row detail page
|
||||
- Fixed an issue where blocks didn't grow when the grid got bigger
|
||||
- Fixed several bugs related to AI writers
|
||||
### Mobile
|
||||
#### New Features
|
||||
- Added sign-in with OTP (One-Time Password)
|
||||
#### Bug Fixes
|
||||
- Fixed an issue where the slash menu sometimes failed to display
|
||||
- Updated the mention page block to handle page selection with more context.
|
||||
|
||||
## Version 0.8.8 - 01/04/2025
|
||||
### New Features
|
||||
- Added support for selecting AI models in AI writer
|
||||
|
||||
Vendored
+124
-124
@@ -1,125 +1,125 @@
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
// This task only builds the Dart code of AppFlowy.
|
||||
// It supports both the desktop and mobile version.
|
||||
"name": "AF: Build Dart Only",
|
||||
"request": "launch",
|
||||
"program": "./lib/main.dart",
|
||||
"type": "dart",
|
||||
"env": {
|
||||
"RUST_LOG": "debug",
|
||||
},
|
||||
// uncomment the following line to testing performance.
|
||||
// "flutterMode": "profile",
|
||||
"cwd": "${workspaceRoot}/appflowy_flutter"
|
||||
},
|
||||
{
|
||||
// This task builds the Rust and Dart code of AppFlowy.
|
||||
"name": "AF-desktop: Build All",
|
||||
"request": "launch",
|
||||
"program": "./lib/main.dart",
|
||||
"type": "dart",
|
||||
"preLaunchTask": "AF: Build Appflowy Core",
|
||||
"env": {
|
||||
"RUST_LOG": "trace",
|
||||
"RUST_BACKTRACE": "1"
|
||||
},
|
||||
"cwd": "${workspaceRoot}/appflowy_flutter"
|
||||
},
|
||||
{
|
||||
// This task builds will:
|
||||
// - call the clean task,
|
||||
// - rebuild all the generated Files (including freeze and language files)
|
||||
// - rebuild the the Rust and Dart code of AppFlowy.
|
||||
"name": "AF-desktop: Clean + Rebuild All",
|
||||
"request": "launch",
|
||||
"program": "./lib/main.dart",
|
||||
"type": "dart",
|
||||
"preLaunchTask": "AF: Clean + Rebuild All",
|
||||
"env": {
|
||||
"RUST_LOG": "trace"
|
||||
},
|
||||
"cwd": "${workspaceRoot}/appflowy_flutter"
|
||||
},
|
||||
{
|
||||
"name": "AF-iOS: Build All",
|
||||
"request": "launch",
|
||||
"program": "./lib/main.dart",
|
||||
"type": "dart",
|
||||
"preLaunchTask": "AF: Build Appflowy Core For iOS",
|
||||
"env": {
|
||||
"RUST_LOG": "trace"
|
||||
},
|
||||
"cwd": "${workspaceRoot}/appflowy_flutter"
|
||||
},
|
||||
{
|
||||
"name": "AF-iOS: Clean + Rebuild All",
|
||||
"request": "launch",
|
||||
"program": "./lib/main.dart",
|
||||
"type": "dart",
|
||||
"preLaunchTask": "AF: Clean + Rebuild All (iOS)",
|
||||
"env": {
|
||||
"RUST_LOG": "trace"
|
||||
},
|
||||
"cwd": "${workspaceRoot}/appflowy_flutter"
|
||||
},
|
||||
{
|
||||
"name": "AF-iOS-Simulator: Build All",
|
||||
"request": "launch",
|
||||
"program": "./lib/main.dart",
|
||||
"type": "dart",
|
||||
"preLaunchTask": "AF: Build Appflowy Core For iOS Simulator",
|
||||
"env": {
|
||||
"RUST_LOG": "trace"
|
||||
},
|
||||
"cwd": "${workspaceRoot}/appflowy_flutter"
|
||||
},
|
||||
{
|
||||
"name": "AF-iOS-Simulator: Clean + Rebuild All",
|
||||
"request": "launch",
|
||||
"program": "./lib/main.dart",
|
||||
"type": "dart",
|
||||
"preLaunchTask": "AF: Clean + Rebuild All (iOS Simulator)",
|
||||
"env": {
|
||||
"RUST_LOG": "trace"
|
||||
},
|
||||
"cwd": "${workspaceRoot}/appflowy_flutter"
|
||||
},
|
||||
{
|
||||
"name": "AF-Android: Build All",
|
||||
"request": "launch",
|
||||
"program": "./lib/main.dart",
|
||||
"type": "dart",
|
||||
"preLaunchTask": "AF: Build Appflowy Core For Android",
|
||||
"env": {
|
||||
"RUST_LOG": "trace"
|
||||
},
|
||||
"cwd": "${workspaceRoot}/appflowy_flutter"
|
||||
},
|
||||
{
|
||||
"name": "AF-Android: Clean + Rebuild All",
|
||||
"request": "launch",
|
||||
"program": "./lib/main.dart",
|
||||
"type": "dart",
|
||||
"preLaunchTask": "AF: Clean + Rebuild All (Android)",
|
||||
"env": {
|
||||
"RUST_LOG": "trace"
|
||||
},
|
||||
"cwd": "${workspaceRoot}/appflowy_flutter"
|
||||
},
|
||||
{
|
||||
"name": "AF-desktop: Debug Rust",
|
||||
"type": "lldb",
|
||||
"request": "attach",
|
||||
"pid": "${command:pickMyProcess}"
|
||||
// To launch the application directly, use the following configuration:
|
||||
// "request": "launch",
|
||||
// "program": "[YOUR_APPLICATION_PATH]",
|
||||
},
|
||||
]
|
||||
}
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
// This task only builds the Dart code of AppFlowy.
|
||||
// It supports both the desktop and mobile version.
|
||||
"name": "AF: Build Dart Only",
|
||||
"request": "launch",
|
||||
"program": "./lib/main.dart",
|
||||
"type": "dart",
|
||||
"env": {
|
||||
"RUST_LOG": "trace",
|
||||
},
|
||||
// uncomment the following line to testing performance.
|
||||
// "flutterMode": "profile",
|
||||
"cwd": "${workspaceRoot}/appflowy_flutter"
|
||||
},
|
||||
{
|
||||
// This task builds the Rust and Dart code of AppFlowy.
|
||||
"name": "AF-desktop: Build All",
|
||||
"request": "launch",
|
||||
"program": "./lib/main.dart",
|
||||
"type": "dart",
|
||||
"preLaunchTask": "AF: Build Appflowy Core",
|
||||
"env": {
|
||||
"RUST_LOG": "trace",
|
||||
"RUST_BACKTRACE": "1"
|
||||
},
|
||||
"cwd": "${workspaceRoot}/appflowy_flutter"
|
||||
},
|
||||
{
|
||||
// This task builds will:
|
||||
// - call the clean task,
|
||||
// - rebuild all the generated Files (including freeze and language files)
|
||||
// - rebuild the the Rust and Dart code of AppFlowy.
|
||||
"name": "AF-desktop: Clean + Rebuild All",
|
||||
"request": "launch",
|
||||
"program": "./lib/main.dart",
|
||||
"type": "dart",
|
||||
"preLaunchTask": "AF: Clean + Rebuild All",
|
||||
"env": {
|
||||
"RUST_LOG": "trace"
|
||||
},
|
||||
"cwd": "${workspaceRoot}/appflowy_flutter"
|
||||
},
|
||||
{
|
||||
"name": "AF-iOS: Build All",
|
||||
"request": "launch",
|
||||
"program": "./lib/main.dart",
|
||||
"type": "dart",
|
||||
"preLaunchTask": "AF: Build Appflowy Core For iOS",
|
||||
"env": {
|
||||
"RUST_LOG": "trace"
|
||||
},
|
||||
"cwd": "${workspaceRoot}/appflowy_flutter"
|
||||
},
|
||||
{
|
||||
"name": "AF-iOS: Clean + Rebuild All",
|
||||
"request": "launch",
|
||||
"program": "./lib/main.dart",
|
||||
"type": "dart",
|
||||
"preLaunchTask": "AF: Clean + Rebuild All (iOS)",
|
||||
"env": {
|
||||
"RUST_LOG": "trace"
|
||||
},
|
||||
"cwd": "${workspaceRoot}/appflowy_flutter"
|
||||
},
|
||||
{
|
||||
"name": "AF-iOS-Simulator: Build All",
|
||||
"request": "launch",
|
||||
"program": "./lib/main.dart",
|
||||
"type": "dart",
|
||||
"preLaunchTask": "AF: Build Appflowy Core For iOS Simulator",
|
||||
"env": {
|
||||
"RUST_LOG": "trace"
|
||||
},
|
||||
"cwd": "${workspaceRoot}/appflowy_flutter"
|
||||
},
|
||||
{
|
||||
"name": "AF-iOS-Simulator: Clean + Rebuild All",
|
||||
"request": "launch",
|
||||
"program": "./lib/main.dart",
|
||||
"type": "dart",
|
||||
"preLaunchTask": "AF: Clean + Rebuild All (iOS Simulator)",
|
||||
"env": {
|
||||
"RUST_LOG": "trace"
|
||||
},
|
||||
"cwd": "${workspaceRoot}/appflowy_flutter"
|
||||
},
|
||||
{
|
||||
"name": "AF-Android: Build All",
|
||||
"request": "launch",
|
||||
"program": "./lib/main.dart",
|
||||
"type": "dart",
|
||||
"preLaunchTask": "AF: Build Appflowy Core For Android",
|
||||
"env": {
|
||||
"RUST_LOG": "trace"
|
||||
},
|
||||
"cwd": "${workspaceRoot}/appflowy_flutter"
|
||||
},
|
||||
{
|
||||
"name": "AF-Android: Clean + Rebuild All",
|
||||
"request": "launch",
|
||||
"program": "./lib/main.dart",
|
||||
"type": "dart",
|
||||
"preLaunchTask": "AF: Clean + Rebuild All (Android)",
|
||||
"env": {
|
||||
"RUST_LOG": "trace"
|
||||
},
|
||||
"cwd": "${workspaceRoot}/appflowy_flutter"
|
||||
},
|
||||
{
|
||||
"name": "AF-desktop: Debug Rust",
|
||||
"type": "lldb",
|
||||
"request": "attach",
|
||||
"pid": "${command:pickMyProcess}"
|
||||
// To launch the application directly, use the following configuration:
|
||||
// "request": "launch",
|
||||
// "program": "[YOUR_APPLICATION_PATH]",
|
||||
},
|
||||
]
|
||||
}
|
||||
+35
-36
@@ -1,16 +1,16 @@
|
||||
#https://github.com/sagiegurari/cargo-make
|
||||
|
||||
extend = [
|
||||
{ path = "scripts/makefile/desktop.toml" },
|
||||
{ path = "scripts/makefile/mobile.toml" },
|
||||
{ path = "scripts/makefile/protobuf.toml" },
|
||||
{ path = "scripts/makefile/tests.toml" },
|
||||
{ path = "scripts/makefile/docker.toml" },
|
||||
{ path = "scripts/makefile/env.toml" },
|
||||
{ path = "scripts/makefile/flutter.toml" },
|
||||
{ path = "scripts/makefile/tool.toml" },
|
||||
{ path = "scripts/makefile/tauri.toml" },
|
||||
{ path = "scripts/makefile/web.toml" },
|
||||
{ path = "scripts/makefile/desktop.toml" },
|
||||
{ path = "scripts/makefile/mobile.toml" },
|
||||
{ path = "scripts/makefile/protobuf.toml" },
|
||||
{ path = "scripts/makefile/tests.toml" },
|
||||
{ path = "scripts/makefile/docker.toml" },
|
||||
{ path = "scripts/makefile/env.toml" },
|
||||
{ path = "scripts/makefile/flutter.toml" },
|
||||
{ path = "scripts/makefile/tool.toml" },
|
||||
{ path = "scripts/makefile/tauri.toml" },
|
||||
{ path = "scripts/makefile/web.toml" },
|
||||
]
|
||||
|
||||
[config]
|
||||
@@ -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.9.1"
|
||||
APPFLOWY_VERSION = "0.8.9"
|
||||
FLUTTER_DESKTOP_FEATURES = "dart"
|
||||
PRODUCT_NAME = "AppFlowy"
|
||||
MACOSX_DEPLOYMENT_TARGET = "11.0"
|
||||
@@ -228,31 +228,30 @@ script = ['''
|
||||
echo APP_ENVIRONMENT: ${APP_ENVIRONMENT}
|
||||
echo BUILD_ARCHS: ${BUILD_ARCHS}
|
||||
echo BUILD_VERSION: ${BUILD_VERSION}
|
||||
echo RUST_VERSION: "$(rustc --version)"
|
||||
''']
|
||||
script_runner = "@shell"
|
||||
|
||||
[tasks.setup-crate-type]
|
||||
private = true
|
||||
script = [
|
||||
"""
|
||||
toml = readfile ${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/rust-lib/${CARGO_MAKE_CRATE_NAME}/Cargo.toml
|
||||
val = replace ${toml} "staticlib" ${CRATE_TYPE}
|
||||
result = writefile ${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/rust-lib/${CARGO_MAKE_CRATE_NAME}/Cargo.toml ${val}
|
||||
assert ${result}
|
||||
""",
|
||||
"""
|
||||
toml = readfile ${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/rust-lib/${CARGO_MAKE_CRATE_NAME}/Cargo.toml
|
||||
val = replace ${toml} "staticlib" ${CRATE_TYPE}
|
||||
result = writefile ${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/rust-lib/${CARGO_MAKE_CRATE_NAME}/Cargo.toml ${val}
|
||||
assert ${result}
|
||||
""",
|
||||
]
|
||||
script_runner = "@duckscript"
|
||||
|
||||
[tasks.restore-crate-type]
|
||||
private = true
|
||||
script = [
|
||||
"""
|
||||
toml = readfile ${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/rust-lib/${CARGO_MAKE_CRATE_NAME}/Cargo.toml
|
||||
val = replace ${toml} ${CRATE_TYPE} "staticlib"
|
||||
result = writefile ${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/rust-lib/${CARGO_MAKE_CRATE_NAME}/Cargo.toml ${val}
|
||||
assert ${result}
|
||||
""",
|
||||
"""
|
||||
toml = readfile ${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/rust-lib/${CARGO_MAKE_CRATE_NAME}/Cargo.toml
|
||||
val = replace ${toml} ${CRATE_TYPE} "staticlib"
|
||||
result = writefile ${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/rust-lib/${CARGO_MAKE_CRATE_NAME}/Cargo.toml ${val}
|
||||
assert ${result}
|
||||
""",
|
||||
]
|
||||
script_runner = "@duckscript"
|
||||
|
||||
@@ -280,24 +279,24 @@ TEST_COMPILE_TARGET = "x86_64-pc-windows-msvc"
|
||||
[tasks.setup-test-crate-type]
|
||||
private = true
|
||||
script = [
|
||||
"""
|
||||
toml = readfile ${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/rust-lib/${CARGO_MAKE_CRATE_NAME}/Cargo.toml
|
||||
val = replace ${toml} "staticlib" ${TEST_CRATE_TYPE}
|
||||
result = writefile ${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/rust-lib/${CARGO_MAKE_CRATE_NAME}/Cargo.toml ${val}
|
||||
assert ${result}
|
||||
""",
|
||||
"""
|
||||
toml = readfile ${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/rust-lib/${CARGO_MAKE_CRATE_NAME}/Cargo.toml
|
||||
val = replace ${toml} "staticlib" ${TEST_CRATE_TYPE}
|
||||
result = writefile ${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/rust-lib/${CARGO_MAKE_CRATE_NAME}/Cargo.toml ${val}
|
||||
assert ${result}
|
||||
""",
|
||||
]
|
||||
script_runner = "@duckscript"
|
||||
|
||||
[tasks.restore-test-crate-type]
|
||||
private = true
|
||||
script = [
|
||||
"""
|
||||
toml = readfile ${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/rust-lib/${CARGO_MAKE_CRATE_NAME}/Cargo.toml
|
||||
val = replace ${toml} ${TEST_CRATE_TYPE} "staticlib"
|
||||
result = writefile ${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/rust-lib/${CARGO_MAKE_CRATE_NAME}/Cargo.toml ${val}
|
||||
assert ${result}
|
||||
""",
|
||||
"""
|
||||
toml = readfile ${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/rust-lib/${CARGO_MAKE_CRATE_NAME}/Cargo.toml
|
||||
val = replace ${toml} ${TEST_CRATE_TYPE} "staticlib"
|
||||
result = writefile ${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/rust-lib/${CARGO_MAKE_CRATE_NAME}/Cargo.toml ${val}
|
||||
assert ${result}
|
||||
""",
|
||||
]
|
||||
script_runner = "@duckscript"
|
||||
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
Whitespace-only changes.
Binary file not shown.
@@ -8,7 +8,7 @@
|
||||
"{""data"":""Juan"",""last_modified"":1726063423,""created_at"":1726063423,""field_type"":0}","{""created_at"":1726122510,""reminder_id"":"""",""include_time"":false,""is_range"":true,""last_modified"":1726122515,""data"":""1725604115"",""end_timestamp"":""1725776915"",""field_type"":2}","{""field_type"":1,""created_at"":1726063677,""last_modified"":1726063677,""data"":""93100""}","{""field_type"":1,""data"":""4.86"",""created_at"":1726062597,""last_modified"":1726062597}",,"{""last_modified"":1726062377,""field_type"":5,""data"":""Yes"",""created_at"":1726062377}","{""last_modified"":1726064412,""field_type"":7,""data"":""{\""options\"":[{\""id\"":\""tTDq\"",\""name\"":\""complete onboarding\"",\""color\"":\""Purple\""},{\""id\"":\""E8Ds\"",\""name\"":\""contact support\"",\""color\"":\""Purple\""},{\""id\"":\""RoGN\"",\""name\"":\""get started\"",\""color\"":\""Purple\""}],\""selected_option_ids\"":[\""tTDq\"",\""E8Ds\""]}"",""created_at"":1726064396}",,"{""created_at"":1726065278,""field_type"":3,""data"":""qnja"",""last_modified"":1726065278}","{""data"":""R9I7,yORP,1i4f"",""field_type"":4,""created_at"":1726105126,""last_modified"":1726105127}","{""data"":""1726122515"",""field_type"":8}","{""data"":""1726060541"",""field_type"":9}"
|
||||
"{""data"":""Alex"",""created_at"":1726063432,""last_modified"":1726063432,""field_type"":0}","{""reminder_id"":"""",""data"":""1725292800"",""include_time"":true,""last_modified"":1726122448,""created_at"":1726122422,""is_range"":true,""end_timestamp"":""1725551940"",""field_type"":2}","{""field_type"":1,""last_modified"":1726063683,""created_at"":1726063683,""data"":""3560""}","{""created_at"":1726062561,""data"":""1.96"",""last_modified"":1726062561,""field_type"":1}","{""last_modified"":1726063952,""created_at"":1726063931,""data"":""al3x1343@protonmail.com"",""field_type"":6}","{""last_modified"":1726062375,""field_type"":5,""created_at"":1726062375,""data"":""Yes""}","{""data"":""{\""options\"":[{\""id\"":\""qNyr\"",\""name\"":\""finish reading book\"",\""color\"":\""Purple\""}],\""selected_option_ids\"":[]}"",""created_at"":1726064616,""last_modified"":1726064616,""field_type"":7}",,"{""data"":""qnja"",""created_at"":1726065272,""last_modified"":1726065272,""field_type"":3}","{""created_at"":1726105180,""last_modified"":1726105180,""field_type"":4,""data"":""R9I7,1i4f""}","{""field_type"":8,""data"":""1726122448""}","{""field_type"":9,""data"":""1726060541""}"
|
||||
"{""last_modified"":1726063478,""created_at"":1726063436,""field_type"":0,""data"":""Alexander""}",,"{""field_type"":1,""last_modified"":1726063691,""created_at"":1726063691,""data"":""2073""}","{""field_type"":1,""data"":""0.5"",""last_modified"":1726062577,""created_at"":1726062577}","{""last_modified"":1726063991,""field_type"":6,""created_at"":1726063991,""data"":""alexandernotthedra@gmail.com""}","{""field_type"":5,""last_modified"":1726062378,""created_at"":1726062377,""data"":""No""}",,,"{""created_at"":1726065291,""data"":""GSf_"",""last_modified"":1726065291,""field_type"":3}","{""last_modified"":1726105142,""created_at"":1726105133,""data"":""SEUo"",""field_type"":4}","{""field_type"":8,""data"":""1726105142""}","{""field_type"":9,""data"":""1726060542""}"
|
||||
"{""field_type"":0,""created_at"":1726063454,""last_modified"":1726063454,""data"":""George""}","{""created_at"":1726122467,""end_timestamp"":""1726468070"",""include_time"":false,""is_range"":true,""reminder_id"":"""",""field_type"":2,""data"":""1726295270"",""last_modified"":1726122470}",,,"{""field_type"":6,""data"":""george.aq@appflowy.io"",""last_modified"":1726064104,""created_at"":1726064016}","{""last_modified"":1726062376,""created_at"":1726062376,""field_type"":5,""data"":""Yes""}","{""data"":""{\""options\"":[{\""id\"":\""s_dQ\"",\""name\"":\""bug triage\"",\""color\"":\""Purple\""},{\""id\"":\""-Zfo\"",\""name\"":\""fix bugs\"",\""color\"":\""Purple\""},{\""id\"":\""wsDN\"",\""name\"":\""attend meetings\"",\""color\"":\""Purple\""}],\""selected_option_ids\"":[\""s_dQ\"",\""-Zfo\""]}"",""last_modified"":1726064468,""created_at"":1726064424,""field_type"":7}","{""data"":[""{\""id\"":\""8a77f84d-64e9-4e67-b902-fa23980459ec\"",\""name\"":\""BQdTmxpRI6f.png\"",\""url\"":\""https://file-examples.com/wp-content/storage/2017/10/file_example_PNG_500kB.png\"",\""upload_type\"":\""NetworkMedia\"",\""file_type\"":\""Image\""}""],""field_type"":14,""created_at"":1726122956,""last_modified"":1726122956}","{""field_type"":3,""data"":""qnja"",""created_at"":1726065313,""last_modified"":1726065313}","{""data"":""R9I7,yORP"",""field_type"":4,""last_modified"":1726105198,""created_at"":1726105187}","{""data"":""1726122956"",""field_type"":8}","{""data"":""1726060543"",""field_type"":9}"
|
||||
"{""field_type"":0,""created_at"":1726063454,""last_modified"":1726063454,""data"":""George""}","{""created_at"":1726122467,""end_timestamp"":""1726468070"",""include_time"":false,""is_range"":true,""reminder_id"":"""",""field_type"":2,""data"":""1726295270"",""last_modified"":1726122470}",,,"{""field_type"":6,""data"":""george.aq@appflowy.io"",""last_modified"":1726064104,""created_at"":1726064016}","{""last_modified"":1726062376,""created_at"":1726062376,""field_type"":5,""data"":""Yes""}","{""data"":""{\""options\"":[{\""id\"":\""s_dQ\"",\""name\"":\""bug triage\"",\""color\"":\""Purple\""},{\""id\"":\""-Zfo\"",\""name\"":\""fix bugs\"",\""color\"":\""Purple\""},{\""id\"":\""wsDN\"",\""name\"":\""attend meetings\"",\""color\"":\""Purple\""}],\""selected_option_ids\"":[\""s_dQ\"",\""-Zfo\""]}"",""last_modified"":1726064468,""created_at"":1726064424,""field_type"":7}","{""data"":[""{\""id\"":\""8a77f84d-64e9-4e67-b902-fa23980459ec\"",\""name\"":\""BQdTmxpRI6f.png\"",\""url\"":\""https://static.cdninstagram.com/rsrc.php/v3/ym/r/BQdTmxpRI6f.png\"",\""upload_type\"":\""NetworkMedia\"",\""file_type\"":\""Image\""}""],""field_type"":14,""created_at"":1726122956,""last_modified"":1726122956}","{""field_type"":3,""data"":""qnja"",""created_at"":1726065313,""last_modified"":1726065313}","{""data"":""R9I7,yORP"",""field_type"":4,""last_modified"":1726105198,""created_at"":1726105187}","{""data"":""1726122956"",""field_type"":8}","{""data"":""1726060543"",""field_type"":9}"
|
||||
"{""field_type"":0,""last_modified"":1726063467,""data"":""Joanna"",""created_at"":1726063467}","{""include_time"":false,""end_timestamp"":""1727072893"",""is_range"":true,""last_modified"":1726122493,""created_at"":1726122483,""data"":""1726554493"",""field_type"":2,""reminder_id"":""""}","{""last_modified"":1726065463,""data"":""16470"",""field_type"":1,""created_at"":1726065463}","{""created_at"":1726062626,""field_type"":1,""last_modified"":1726062626,""data"":""-5.36""}","{""last_modified"":1726064069,""data"":""joannastrawberry29+hello@gmail.com"",""created_at"":1726064069,""field_type"":6}",,"{""field_type"":7,""created_at"":1726064444,""last_modified"":1726064460,""data"":""{\""options\"":[{\""id\"":\""ZxJz\"",\""name\"":\""post on Twitter\"",\""color\"":\""Purple\""},{\""id\"":\""upwi\"",\""name\"":\""watch Youtube videos\"",\""color\"":\""Purple\""}],\""selected_option_ids\"":[\""upwi\""]}""}",,"{""created_at"":1726065317,""last_modified"":1726065317,""field_type"":3,""data"":""qnja""}","{""field_type"":4,""last_modified"":1726105173,""data"":""uRAO,yORP"",""created_at"":1726105170}","{""data"":""1726122493"",""field_type"":8}","{""data"":""1726060545"",""field_type"":9}"
|
||||
"{""last_modified"":1726063457,""created_at"":1726063457,""data"":""George"",""field_type"":0}","{""include_time"":true,""reminder_id"":"""",""field_type"":2,""is_range"":true,""created_at"":1726122521,""end_timestamp"":""1725829200"",""data"":""1725822900"",""last_modified"":1726122535}","{""last_modified"":1726065493,""field_type"":1,""data"":""9500"",""created_at"":1726065493}","{""last_modified"":1726062680,""created_at"":1726062680,""field_type"":1,""data"":""1.7""}","{""data"":""plgeorgebball@gmail.com"",""field_type"":6,""last_modified"":1726064087,""created_at"":1726064036}",,"{""last_modified"":1726064513,""data"":""{\""options\"":[{\""id\"":\""zy0x\"",\""name\"":\""game vs celtics\"",\""color\"":\""Purple\""},{\""id\"":\""WJsv\"",\""name\"":\""training\"",\""color\"":\""Purple\""},{\""id\"":\""w-f8\"",\""name\"":\""game vs spurs\"",\""color\"":\""Purple\""},{\""id\"":\""p1VQ\"",\""name\"":\""game vs knicks\"",\""color\"":\""Purple\""},{\""id\"":\""VjUA\"",\""name\"":\""recovery\"",\""color\"":\""Purple\""},{\""id\"":\""sQ8X\"",\""name\"":\""don't get injured\"",\""color\"":\""Purple\""}],\""selected_option_ids\"":[]}"",""created_at"":1726064486,""field_type"":7}",,"{""field_type"":3,""last_modified"":1726065310,""data"":""qnja"",""created_at"":1726065310}","{""created_at"":1726105205,""field_type"":4,""last_modified"":1726105249,""data"":""R9I7,1i4f,yORP,SEUo""}","{""data"":""1726122535"",""field_type"":8}","{""field_type"":9,""data"":""1726060546""}"
|
||||
"{""data"":""Judy"",""created_at"":1726063475,""field_type"":0,""last_modified"":1726063487}","{""end_timestamp"":"""",""reminder_id"":"""",""data"":""1726640950"",""field_type"":2,""include_time"":false,""created_at"":1726122550,""last_modified"":1726122550,""is_range"":false}",,,"{""created_at"":1726063882,""field_type"":6,""last_modified"":1726064000,""data"":""judysmithjr@outlook.com""}","{""last_modified"":1726062712,""field_type"":5,""data"":""Yes"",""created_at"":1726062712}","{""created_at"":1726064549,""field_type"":7,""data"":""{\""options\"":[{\""id\"":\""j8cC\"",\""name\"":\""finish training\"",\""color\"":\""Purple\""},{\""id\"":\""SmSk\"",\""name\"":\""brainwash\"",\""color\"":\""Purple\""},{\""id\"":\""mnf5\"",\""name\"":\""welcome to ba sing se\"",\""color\"":\""Purple\""},{\""id\"":\""hcrj\"",\""name\"":\""don't mess up\"",\""color\"":\""Purple\""}],\""selected_option_ids\"":[\""j8cC\"",\""SmSk\"",\""mnf5\"",\""hcrj\""]}"",""last_modified"":1726064591}",,,"{""field_type"":4,""last_modified"":1726105152,""created_at"":1726105152,""data"":""R9I7""}","{""field_type"":8,""data"":""1726122550""}","{""field_type"":9,""data"":""1726060549""}"
|
||||
@@ -12,38 +12,24 @@ import 'uncategorized/uncategorized_test_runner.dart'
|
||||
import 'workspace/workspace_test_runner.dart' as workspace_test_runner;
|
||||
|
||||
Future<void> main() async {
|
||||
// don't remove this test, it can prevent the test from failing.
|
||||
{
|
||||
preset_af_cloud_env_test.main();
|
||||
data_migration_test_runner.main();
|
||||
preset_af_cloud_env_test.main();
|
||||
|
||||
// uncategorized
|
||||
uncategorized_test_runner.main();
|
||||
data_migration_test_runner.main();
|
||||
|
||||
// workspace
|
||||
workspace_test_runner.main();
|
||||
}
|
||||
// uncategorized
|
||||
uncategorized_test_runner.main();
|
||||
|
||||
// sidebar
|
||||
// don't remove this test, it can prevent the test from failing.
|
||||
{
|
||||
preset_af_cloud_env_test.main();
|
||||
sidebar_move_page_test.main();
|
||||
sidebar_rename_untitled_test.main();
|
||||
sidebar_icon_test.main();
|
||||
}
|
||||
|
||||
// database
|
||||
// don't remove this test, it can prevent the test from failing.
|
||||
{
|
||||
preset_af_cloud_env_test.main();
|
||||
database_test_runner.main();
|
||||
}
|
||||
// workspace
|
||||
workspace_test_runner.main();
|
||||
|
||||
// document
|
||||
// don't remove this test, it can prevent the test from failing.
|
||||
{
|
||||
preset_af_cloud_env_test.main();
|
||||
document_test_runner.main();
|
||||
}
|
||||
document_test_runner.main();
|
||||
|
||||
// sidebar
|
||||
sidebar_move_page_test.main();
|
||||
sidebar_rename_untitled_test.main();
|
||||
sidebar_icon_test.main();
|
||||
|
||||
// database
|
||||
database_test_runner.main();
|
||||
}
|
||||
+3
-1
@@ -1,3 +1,5 @@
|
||||
import 'anon_user_data_migration_test.dart' as anon_user_test;
|
||||
|
||||
void main() async {
|
||||
// anon_user_test.main();
|
||||
anon_user_test.main();
|
||||
}
|
||||
-187
@@ -1,19 +1,12 @@
|
||||
import 'package:appflowy/ai/service/ai_entities.dart';
|
||||
import 'package:appflowy/env/cloud_env.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_entities.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-ai/entities.pbenum.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
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 '../../../shared/ai_test_op.dart';
|
||||
import '../../../shared/constants.dart';
|
||||
import '../../../shared/mock/mock_ai.dart';
|
||||
import '../../../shared/util.dart';
|
||||
|
||||
void main() {
|
||||
@@ -24,7 +17,6 @@ void main() {
|
||||
(tester) async {
|
||||
await tester.initializeAppFlowy(
|
||||
cloudType: AuthenticatorType.appflowyCloudSelfHost,
|
||||
aiRepositoryBuilder: () => MockAIRepository(),
|
||||
);
|
||||
await tester.tapGoogleLoginInButton();
|
||||
await tester.expectToSeeHomePageWithGetStartedPage();
|
||||
@@ -51,184 +43,5 @@ void main() {
|
||||
// expect the ai writer block is not in the document
|
||||
expect(find.byType(AiWriterBlockComponent), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('Improve writing', (tester) async {
|
||||
await tester.initializeAppFlowy(
|
||||
cloudType: AuthenticatorType.appflowyCloudSelfHost,
|
||||
);
|
||||
await tester.tapGoogleLoginInButton();
|
||||
await tester.expectToSeeHomePageWithGetStartedPage();
|
||||
|
||||
const pageName = 'Document';
|
||||
await tester.createNewPageInSpace(
|
||||
spaceName: Constants.generalSpaceName,
|
||||
layout: ViewLayoutPB.Document,
|
||||
pageName: pageName,
|
||||
);
|
||||
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
|
||||
// insert a paragraph
|
||||
final text = 'I have an apple';
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
await tester.ime.insertText(text);
|
||||
await tester.editor.updateSelection(
|
||||
Selection(
|
||||
start: Position(path: [0]),
|
||||
end: Position(path: [0], offset: text.length),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tapButton(find.byType(ImproveWritingButton));
|
||||
|
||||
final editorState = tester.editor.getCurrentEditorState();
|
||||
final document = editorState.document;
|
||||
|
||||
expect(document.root.children.length, 3);
|
||||
expect(document.root.children[1].type, ParagraphBlockKeys.type);
|
||||
expect(
|
||||
document.root.children[1].delta!.toPlainText(),
|
||||
'I have an apple and a banana',
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('fix grammar', (tester) async {
|
||||
await tester.initializeAppFlowy(
|
||||
cloudType: AuthenticatorType.appflowyCloudSelfHost,
|
||||
);
|
||||
await tester.tapGoogleLoginInButton();
|
||||
await tester.expectToSeeHomePageWithGetStartedPage();
|
||||
|
||||
const pageName = 'Document';
|
||||
await tester.createNewPageInSpace(
|
||||
spaceName: Constants.generalSpaceName,
|
||||
layout: ViewLayoutPB.Document,
|
||||
pageName: pageName,
|
||||
);
|
||||
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
|
||||
// insert a paragraph
|
||||
final text = 'We didn’t had enough money';
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
await tester.ime.insertText(text);
|
||||
await tester.editor.updateSelection(
|
||||
Selection(
|
||||
start: Position(path: [0]),
|
||||
end: Position(path: [0], offset: text.length),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
await tester.selectAIWriter(AiWriterCommand.fixSpellingAndGrammar);
|
||||
|
||||
final editorState = tester.editor.getCurrentEditorState();
|
||||
final document = editorState.document;
|
||||
|
||||
expect(document.root.children.length, 3);
|
||||
expect(document.root.children[1].type, ParagraphBlockKeys.type);
|
||||
expect(
|
||||
document.root.children[1].delta!.toPlainText(),
|
||||
'We didn’t have enough money',
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('ask ai', (tester) async {
|
||||
await tester.initializeAppFlowy(
|
||||
cloudType: AuthenticatorType.appflowyCloudDevelop,
|
||||
aiRepositoryBuilder: () => MockAIRepository(
|
||||
validator: _CompletionHistoryValidator(),
|
||||
),
|
||||
);
|
||||
await tester.tapGoogleLoginInButton();
|
||||
await tester.expectToSeeHomePageWithGetStartedPage();
|
||||
|
||||
const pageName = 'Document';
|
||||
await tester.createNewPageInSpace(
|
||||
spaceName: Constants.generalSpaceName,
|
||||
layout: ViewLayoutPB.Document,
|
||||
pageName: pageName,
|
||||
);
|
||||
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
|
||||
// insert a paragraph
|
||||
final text = 'What is TPU?';
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
await tester.ime.insertText(text);
|
||||
await tester.editor.updateSelection(
|
||||
Selection(
|
||||
start: Position(path: [0]),
|
||||
end: Position(path: [0], offset: text.length),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
await tester.selectAIWriter(AiWriterCommand.userQuestion);
|
||||
|
||||
await tester.enterTextInPromptTextField("Explain the concept of TPU");
|
||||
await tester.simulateKeyEvent(LogicalKeyboardKey.enter);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// await tester.selectModel("GPT-4o-mini");
|
||||
|
||||
await tester.enterTextInPromptTextField("How about GPU?");
|
||||
await tester.simulateKeyEvent(LogicalKeyboardKey.enter);
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
class _CompletionHistoryValidator extends StreamCompletionValidator {
|
||||
static const _expectedResponses = {
|
||||
1: "What is TPU?",
|
||||
3: [
|
||||
"What is TPU?",
|
||||
"Explain the concept of TPU",
|
||||
"TPU is a tensor processing unit that is designed to accelerate machine",
|
||||
],
|
||||
5: [
|
||||
"What is TPU?",
|
||||
"Explain the concept of TPU",
|
||||
"TPU is a tensor processing unit that is designed to accelerate machine",
|
||||
"How about GPU?",
|
||||
"GPU is a graphics processing unit that is designed to accelerate machine learning tasks.",
|
||||
],
|
||||
};
|
||||
|
||||
@override
|
||||
bool validate(
|
||||
String text,
|
||||
String? objectId,
|
||||
CompletionTypePB completionType,
|
||||
PredefinedFormat? format,
|
||||
List<String> sourceIds,
|
||||
List<AiWriterRecord> history,
|
||||
) {
|
||||
assert(completionType == CompletionTypePB.UserQuestion);
|
||||
if (history.isEmpty) return false;
|
||||
|
||||
final expectedMessages = _expectedResponses[history.length];
|
||||
if (expectedMessages == null) return false;
|
||||
|
||||
if (expectedMessages is String) {
|
||||
_assertMessage(history[0].content, expectedMessages);
|
||||
return true;
|
||||
} else if (expectedMessages is List<String>) {
|
||||
for (var i = 0; i < expectedMessages.length; i++) {
|
||||
_assertMessage(history[i].content, expectedMessages[i]);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void _assertMessage(String actual, String expected) {
|
||||
assert(
|
||||
actual.trim() == expected,
|
||||
"expected '$expected', but got '${actual.trim()}'",
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -8,13 +8,11 @@ import '../../shared/util.dart';
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('Preset cloud env', () {
|
||||
testWidgets('use self-hosted cloud', (tester) async {
|
||||
group('Empty', () {
|
||||
testWidgets('set appflowy cloud', (tester) async {
|
||||
await tester.initializeAppFlowy(
|
||||
cloudType: AuthenticatorType.appflowyCloudSelfHost,
|
||||
);
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
});
|
||||
}
|
||||
+2
-2
@@ -39,8 +39,8 @@ void main() {
|
||||
);
|
||||
await tester.tapButton(find.byType(AccountSignInOutButton));
|
||||
|
||||
tester.expectToSeeText(LocaleKeys.button_yes.tr());
|
||||
await tester.tapButtonWithName(LocaleKeys.button_yes.tr());
|
||||
tester.expectToSeeText(LocaleKeys.button_ok.tr());
|
||||
await tester.tapButtonWithName(LocaleKeys.button_ok.tr());
|
||||
|
||||
// Go to the sign in page again
|
||||
await tester.pumpAndSettle(const Duration(seconds: 5));
|
||||
|
||||
@@ -80,8 +80,8 @@ void main() {
|
||||
await tester.tapGoogleLoginInButton();
|
||||
await tester.expectToSeeHomePageWithGetStartedPage();
|
||||
|
||||
expect(find.byType(AppFlowyEditorPage), findsOneWidget);
|
||||
expect(find.text('Blank page'), findsNothing);
|
||||
expect(find.byType(AppFlowyEditorPage), findsNothing);
|
||||
expect(find.text('Blank page'), findsOne);
|
||||
});
|
||||
});
|
||||
}
|
||||
+9
-9
@@ -6,7 +6,7 @@ import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/shared/icon_emoji_picker/icon_picker.dart';
|
||||
import 'package:appflowy/workspace/presentation/command_palette/command_palette.dart';
|
||||
import 'package:appflowy/workspace/presentation/command_palette/widgets/search_field.dart';
|
||||
import 'package:appflowy/workspace/presentation/command_palette/widgets/search_result_cell.dart';
|
||||
import 'package:appflowy/workspace/presentation/command_palette/widgets/search_result_tile.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pbenum.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
@@ -44,12 +44,12 @@ void main() {
|
||||
await tester.pumpAndSettle(const Duration(milliseconds: 200));
|
||||
|
||||
// Expect two search results "ViewOna" and "ViewOne" (Distance 1 to ViewOna)
|
||||
expect(find.byType(SearchResultCell), findsNWidgets(2));
|
||||
expect(find.byType(SearchResultTile), findsNWidgets(2));
|
||||
|
||||
// The score should be higher for "ViewOna" thus it should be shown first
|
||||
final secondDocumentWidget = tester
|
||||
.widget(find.byType(SearchResultCell).first) as SearchResultCell;
|
||||
expect(secondDocumentWidget.item.displayName, secondDocument);
|
||||
.widget(find.byType(SearchResultTile).first) as SearchResultTile;
|
||||
expect(secondDocumentWidget.result.data, secondDocument);
|
||||
|
||||
// Change search to "ViewOne"
|
||||
await tester.enterText(searchFieldFinder, firstDocument);
|
||||
@@ -57,9 +57,9 @@ void main() {
|
||||
|
||||
// The score should be higher for "ViewOne" thus it should be shown first
|
||||
final firstDocumentWidget = tester.widget(
|
||||
find.byType(SearchResultCell).first,
|
||||
) as SearchResultCell;
|
||||
expect(firstDocumentWidget.item.displayName, firstDocument);
|
||||
find.byType(SearchResultTile).first,
|
||||
) as SearchResultTile;
|
||||
expect(firstDocumentWidget.result.data, firstDocument);
|
||||
});
|
||||
|
||||
testWidgets('Displaying icons in search results', (tester) async {
|
||||
@@ -89,11 +89,11 @@ void main() {
|
||||
);
|
||||
await tester.enterText(searchFieldFinder, 'Page-$randomValue');
|
||||
await tester.pumpAndSettle(const Duration(milliseconds: 200));
|
||||
expect(find.byType(SearchResultCell), findsNWidgets(2));
|
||||
expect(find.byType(SearchResultTile), findsNWidgets(2));
|
||||
|
||||
/// check results
|
||||
final svgs = find.descendant(
|
||||
of: find.byType(SearchResultCell),
|
||||
of: find.byType(SearchResultTile),
|
||||
matching: find.byType(FlowySvg),
|
||||
);
|
||||
expect(svgs, findsNWidgets(2));
|
||||
|
||||
+3
-4
@@ -1,5 +1,5 @@
|
||||
import 'package:appflowy/workspace/presentation/command_palette/command_palette.dart';
|
||||
import 'package:appflowy/workspace/presentation/command_palette/widgets/search_recent_view_cell.dart';
|
||||
import 'package:appflowy/workspace/presentation/command_palette/widgets/recent_view_tile.dart';
|
||||
import 'package:appflowy/workspace/presentation/command_palette/widgets/recent_views_list.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
@@ -27,12 +27,11 @@ void main() {
|
||||
expect(find.byType(RecentViewsList), findsOneWidget);
|
||||
|
||||
// Expect three recent history items
|
||||
expect(find.byType(SearchRecentViewCell), findsNWidgets(3));
|
||||
expect(find.byType(RecentViewTile), findsNWidgets(3));
|
||||
|
||||
// Expect the first item to be the last viewed document
|
||||
final firstDocumentWidget =
|
||||
tester.widget(find.byType(SearchRecentViewCell).first)
|
||||
as SearchRecentViewCell;
|
||||
tester.widget(find.byType(RecentViewTile).first) as RecentViewTile;
|
||||
expect(firstDocumentWidget.view.name, secondDocument);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/database/grid/presentation/grid_page.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pbenum.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter_animate/flutter_animate.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
@@ -78,37 +73,5 @@ void main() {
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
|
||||
testWidgets('insert grid in column', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
|
||||
/// create page and show slash menu
|
||||
await tester.createNewPageWithNameUnderParent(name: 'test page');
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
await tester.editor.showSlashMenu();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
/// create a column
|
||||
await tester.editor.tapSlashMenuItemWithName(
|
||||
LocaleKeys.document_slashMenu_name_twoColumns.tr(),
|
||||
);
|
||||
final actionList = find.byType(BlockActionList);
|
||||
expect(actionList, findsNWidgets(2));
|
||||
final position = tester.getCenter(actionList.last);
|
||||
|
||||
/// tap the second child of column
|
||||
await tester.tapAt(position.copyWith(dx: position.dx + 50));
|
||||
|
||||
/// create a grid
|
||||
await tester.editor.showSlashMenu();
|
||||
await tester.pumpAndSettle();
|
||||
await tester.editor.tapSlashMenuItemWithName(
|
||||
LocaleKeys.document_slashMenu_name_grid.tr(),
|
||||
);
|
||||
|
||||
final grid = find.byType(GridPageContent);
|
||||
expect(grid, findsOneWidget);
|
||||
});
|
||||
});
|
||||
}
|
||||
+13
-3
@@ -347,9 +347,8 @@ void main() {
|
||||
await tester.tapButton(menu);
|
||||
|
||||
final convertToLinkButton = find.text(
|
||||
LocaleKeys.document_plugins_linkPreview_linkPreviewMenu_toUrl
|
||||
.tr(),
|
||||
);
|
||||
LocaleKeys.document_plugins_linkPreview_linkPreviewMenu_toUrl.tr(),
|
||||
);
|
||||
expect(convertToLinkButton, findsOneWidget);
|
||||
await tester.tapButton(convertToLinkButton);
|
||||
},
|
||||
@@ -385,6 +384,7 @@ void main() {
|
||||
expect(node.attributes[LinkPreviewBlockKeys.url], url);
|
||||
});
|
||||
|
||||
|
||||
await tester.simulateKeyEvent(
|
||||
LogicalKeyboardKey.keyZ,
|
||||
isControlPressed:
|
||||
@@ -483,6 +483,16 @@ void main() {
|
||||
});
|
||||
});
|
||||
|
||||
testWidgets('paste image url without extension', (tester) async {
|
||||
const plainText =
|
||||
'https://images.unsplash.com/photo-1469474968028-56623f02e42e?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&dl=david-marcu-78A265wPiO4-unsplash.jpg&w=640';
|
||||
await tester.pasteContent(plainText: plainText, (editorState) {
|
||||
final node = editorState.getNodeAtPath([0])!;
|
||||
expect(node.type, ImageBlockKeys.type);
|
||||
expect(node.attributes[ImageBlockKeys.url], isNotEmpty);
|
||||
});
|
||||
});
|
||||
|
||||
const testMarkdownText = '''
|
||||
# I'm h1
|
||||
## I'm h2
|
||||
|
||||
-2
@@ -13,8 +13,6 @@ void main() {
|
||||
await tester.initializeAppFlowy();
|
||||
|
||||
await tester.tapAnonymousSignInButton();
|
||||
final finder = find.text(gettingStarted, findRichText: true);
|
||||
await tester.pumpUntilFound(finder, timeout: const Duration(seconds: 2));
|
||||
|
||||
// create a new document
|
||||
const pageName = 'Test Document';
|
||||
|
||||
+4
-5
@@ -5,7 +5,6 @@ import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_hover_menu.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/link_embed/link_embed_block_component.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/link_embed/link_embed_menu.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/link_preview/custom_link_preview_block_component.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/link_preview/link_preview_menu.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/link_preview/paste_as/paste_as_menu.dart';
|
||||
@@ -398,7 +397,7 @@ void main() {
|
||||
|
||||
Future<void> hoverAndConvert(
|
||||
WidgetTester tester,
|
||||
LinkEmbedConvertCommand command,
|
||||
PasteMenuType command,
|
||||
) async {
|
||||
final embed = find.byType(LinkEmbedBlockComponent);
|
||||
expect(embed, findsOneWidget);
|
||||
@@ -426,7 +425,7 @@ void main() {
|
||||
final link = avaliableLink;
|
||||
await preparePage(tester);
|
||||
await pasteAsEmbed(tester, link);
|
||||
await hoverAndConvert(tester, LinkEmbedConvertCommand.toMention);
|
||||
await hoverAndConvert(tester, PasteMenuType.mention);
|
||||
final node = tester.editor.getNodeAtPath([0]);
|
||||
checkMention(node, link);
|
||||
});
|
||||
@@ -435,7 +434,7 @@ void main() {
|
||||
final link = avaliableLink;
|
||||
await preparePage(tester);
|
||||
await pasteAsEmbed(tester, link);
|
||||
await hoverAndConvert(tester, LinkEmbedConvertCommand.toURL);
|
||||
await hoverAndConvert(tester, PasteMenuType.url);
|
||||
final node = tester.editor.getNodeAtPath([0]);
|
||||
checkUrl(node, link);
|
||||
});
|
||||
@@ -445,7 +444,7 @@ void main() {
|
||||
final link = avaliableLink;
|
||||
await preparePage(tester);
|
||||
await pasteAsEmbed(tester, link);
|
||||
await hoverAndConvert(tester, LinkEmbedConvertCommand.toBookmark);
|
||||
await hoverAndConvert(tester, PasteMenuType.bookmark);
|
||||
final node = tester.editor.getNodeAtPath([0]);
|
||||
checkBookmark(node, link);
|
||||
});
|
||||
|
||||
+8
-8
@@ -237,11 +237,11 @@ void main() {
|
||||
await tester.tapButton(find.byType(MentionDateBlock));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// set date to be fifteenth of the last month
|
||||
// set date to be fifteenth of the next month
|
||||
await tester.tap(
|
||||
find.descendant(
|
||||
of: find.byType(DesktopAppFlowyDatePicker),
|
||||
matching: find.byFlowySvg(FlowySvgs.arrow_left_s),
|
||||
matching: find.byFlowySvg(FlowySvgs.arrow_right_s),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
@@ -275,9 +275,9 @@ void main() {
|
||||
timeFormat: UserTimeFormatPB.TwentyFourHour,
|
||||
);
|
||||
final now = DateTime.now();
|
||||
final fifteenthOfLastMonth = DateTime(now.year, now.month - 1, 15);
|
||||
final fifteenthOfNextMonth = DateTime(now.year, now.month + 1, 15);
|
||||
final formattedDate =
|
||||
dateTimeSettings.dateFormat.formatDate(fifteenthOfLastMonth, false);
|
||||
dateTimeSettings.dateFormat.formatDate(fifteenthOfNextMonth, false);
|
||||
|
||||
expect(find.byType(MentionDateBlock), findsOneWidget);
|
||||
expect(find.text('@$formattedDate'), findsOneWidget);
|
||||
@@ -378,11 +378,11 @@ void main() {
|
||||
await tester.tapButton(find.byType(MentionDateBlock));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// set date to be fifteenth of the last month
|
||||
// set date to be fifteenth of the next month
|
||||
await tester.tap(
|
||||
find.descendant(
|
||||
of: find.byType(DesktopAppFlowyDatePicker),
|
||||
matching: find.byFlowySvg(FlowySvgs.arrow_left_s),
|
||||
matching: find.byFlowySvg(FlowySvgs.arrow_right_s),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
@@ -416,9 +416,9 @@ void main() {
|
||||
timeFormat: UserTimeFormatPB.TwentyFourHour,
|
||||
);
|
||||
final now = DateTime.now();
|
||||
final fifteenthOfLastMonth = DateTime(now.year, now.month - 1, 15);
|
||||
final fifteenthOfNextMonth = DateTime(now.year, now.month + 1, 15);
|
||||
final formattedDate =
|
||||
dateTimeSettings.dateFormat.formatDate(fifteenthOfLastMonth, false);
|
||||
dateTimeSettings.dateFormat.formatDate(fifteenthOfNextMonth, false);
|
||||
|
||||
expect(find.byType(MentionDateBlock), findsOneWidget);
|
||||
expect(find.text('@$formattedDate'), findsOneWidget);
|
||||
|
||||
-1
@@ -176,7 +176,6 @@ Future<void> insertOutlineInDocument(WidgetTester tester) async {
|
||||
await tester.editor.showSlashMenu();
|
||||
await tester.editor.tapSlashMenuItemWithName(
|
||||
LocaleKeys.document_slashMenu_name_outline.tr(),
|
||||
offset: 180,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
+26
-42
@@ -1,63 +1,44 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/emoji/emoji_handler.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/emoji_picker/emoji_picker.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:appflowy_editor/src/editor/editor_component/service/editor.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import '../../shared/keyboard.dart';
|
||||
import '../../shared/util.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
Future<void> prepare(WidgetTester tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
await tester.createNewPageWithNameUnderParent();
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
}
|
||||
|
||||
// May be better to move this to an existing test but unsure what it fits with
|
||||
group('Keyboard shortcuts related to emojis', () {
|
||||
testWidgets('cmd/ctrl+alt+e shortcut opens the emoji picker',
|
||||
(tester) async {
|
||||
await prepare(tester);
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
|
||||
expect(find.byType(EmojiHandler), findsNothing);
|
||||
final Finder editor = find.byType(AppFlowyEditor);
|
||||
await tester.tap(editor);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.simulateKeyEvent(
|
||||
LogicalKeyboardKey.keyE,
|
||||
isAltPressed: true,
|
||||
isMetaPressed: Platform.isMacOS,
|
||||
isControlPressed: !Platform.isMacOS,
|
||||
expect(find.byType(EmojiSelectionMenu), findsNothing);
|
||||
|
||||
await FlowyTestKeyboard.simulateKeyDownEvent(
|
||||
[
|
||||
Platform.isMacOS
|
||||
? LogicalKeyboardKey.meta
|
||||
: LogicalKeyboardKey.control,
|
||||
LogicalKeyboardKey.alt,
|
||||
LogicalKeyboardKey.keyE,
|
||||
],
|
||||
tester: tester,
|
||||
);
|
||||
await tester.pumpAndSettle(Duration(seconds: 1));
|
||||
expect(find.byType(EmojiHandler), findsOneWidget);
|
||||
|
||||
/// press backspace to hide the emoji picker
|
||||
await tester.simulateKeyEvent(LogicalKeyboardKey.backspace);
|
||||
expect(find.byType(EmojiHandler), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('insert emoji by slash menu', (tester) async {
|
||||
await prepare(tester);
|
||||
await tester.editor.showSlashMenu();
|
||||
|
||||
/// show emoji picler
|
||||
await tester.editor.tapSlashMenuItemWithName(
|
||||
LocaleKeys.document_slashMenu_name_emoji.tr(),
|
||||
offset: 100,
|
||||
);
|
||||
await tester.pumpAndSettle(Duration(seconds: 1));
|
||||
expect(find.byType(EmojiHandler), findsOneWidget);
|
||||
await tester.simulateKeyEvent(LogicalKeyboardKey.enter);
|
||||
final firstNode =
|
||||
tester.editor.getCurrentEditorState().getNodeAtPath([0])!;
|
||||
|
||||
/// except the emoji is in document
|
||||
expect(firstNode.delta!.toPlainText().contains('😀'), true);
|
||||
expect(find.byType(EmojiSelectionMenu), findsOneWidget);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -66,7 +47,10 @@ void main() {
|
||||
WidgetTester tester, {
|
||||
String? search,
|
||||
}) async {
|
||||
await prepare(tester);
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
await tester.createNewPageWithNameUnderParent();
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
await tester.ime.insertText(':${search ?? 'a'}');
|
||||
await tester.pumpAndSettle(Duration(seconds: 1));
|
||||
}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/workspace/application/settings/prelude.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/settings_dialog.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
@@ -37,7 +38,7 @@ void main() {
|
||||
LocaleKeys.settings_workspacePage_appearance_options_light.tr(),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle(const Duration(milliseconds: 250));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
themeMode = tester.widget<MaterialApp>(appFinder).themeMode;
|
||||
expect(themeMode, ThemeMode.light);
|
||||
@@ -47,7 +48,7 @@ void main() {
|
||||
LocaleKeys.settings_workspacePage_appearance_options_dark.tr(),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle(const Duration(milliseconds: 250));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
themeMode = tester.widget<MaterialApp>(appFinder).themeMode;
|
||||
expect(themeMode, ThemeMode.dark);
|
||||
@@ -65,11 +66,10 @@ void main() {
|
||||
],
|
||||
tester: tester,
|
||||
);
|
||||
await tester.pumpAndSettle(const Duration(milliseconds: 500));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// disable it temporarily. It works on macOS but not on Linux.
|
||||
// themeMode = tester.widget<MaterialApp>(appFinder).themeMode;
|
||||
// expect(themeMode, ThemeMode.light);
|
||||
themeMode = tester.widget<MaterialApp>(appFinder).themeMode;
|
||||
expect(themeMode, ThemeMode.light);
|
||||
});
|
||||
|
||||
testWidgets('show or hide home menu', (tester) async {
|
||||
|
||||
+19
-15
@@ -1,8 +1,13 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/startup/tasks/prelude.dart';
|
||||
import 'package:appflowy/workspace/application/settings/prelude.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import '../../shared/util.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
@@ -86,24 +91,23 @@ void main() {
|
||||
// }
|
||||
// });
|
||||
|
||||
// Disable this test because it failed after executing.
|
||||
// testWidgets('reset to default location', (tester) async {
|
||||
// await tester.initializeAppFlowy();
|
||||
testWidgets('reset to default location', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
|
||||
// await tester.tapAnonymousSignInButton();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
|
||||
// // home and readme document
|
||||
// await tester.expectToSeeHomePageWithGetStartedPage();
|
||||
// home and readme document
|
||||
await tester.expectToSeeHomePageWithGetStartedPage();
|
||||
|
||||
// // open settings and restore the location
|
||||
// await tester.openSettings();
|
||||
// await tester.openSettingsPage(SettingsPage.manageData);
|
||||
// await tester.restoreLocation();
|
||||
// open settings and restore the location
|
||||
await tester.openSettings();
|
||||
await tester.openSettingsPage(SettingsPage.manageData);
|
||||
await tester.restoreLocation();
|
||||
|
||||
// expect(
|
||||
// await appFlowyApplicationDataDirectory().then((value) => value.path),
|
||||
// await getIt<ApplicationDataStorage>().getPath(),
|
||||
// );
|
||||
// });
|
||||
expect(
|
||||
await appFlowyApplicationDataDirectory().then((value) => value.path),
|
||||
await getIt<ApplicationDataStorage>().getPath(),
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1,11 +1,12 @@
|
||||
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_edit_link_widget.dart';
|
||||
import 'package:appflowy/mobile/presentation/editor/mobile_editor_screen.dart';
|
||||
import 'package:appflowy/mobile/presentation/mobile_bottom_navigation_bar.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/appflowy_mobile_toolbar_item.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/util.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text_field.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
@@ -14,125 +15,102 @@ import '../../shared/util.dart';
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
Future<void> createNeaPage(WidgetTester tester) async {
|
||||
final createPageButton =
|
||||
find.byKey(BottomNavigationBarItemType.add.valueKey);
|
||||
await tester.tapButton(createPageButton);
|
||||
expect(find.byType(MobileDocumentScreen), findsOneWidget);
|
||||
final editor = find.byType(AppFlowyEditor);
|
||||
expect(editor, findsOneWidget);
|
||||
}
|
||||
|
||||
const testLink = 'https://appflowy.io/';
|
||||
|
||||
group('links', () {
|
||||
group('toolbar menu:', () {
|
||||
testWidgets('insert links', (tester) async {
|
||||
await tester.launchInAnonymousMode();
|
||||
|
||||
await createNeaPage(tester);
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
final createPageButton = find.byKey(
|
||||
BottomNavigationBarItemType.add.valueKey,
|
||||
);
|
||||
await tester.tapButton(createPageButton);
|
||||
expect(find.byType(MobileDocumentScreen), findsOneWidget);
|
||||
|
||||
final editor = find.byType(AppFlowyEditor);
|
||||
expect(editor, findsOneWidget);
|
||||
final editorState = tester.editor.getCurrentEditorState();
|
||||
|
||||
/// move cursor to content
|
||||
final root = editorState.document.root;
|
||||
final lastNode = root.children.lastOrNull;
|
||||
await editorState.updateSelectionWithReason(
|
||||
Selection.collapsed(Position(path: lastNode!.path)),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
/// insert two lines of text
|
||||
const strFirst = 'FirstLine', strSecond = 'SecondLine';
|
||||
await tester.ime.insertText(strFirst);
|
||||
const strFirst = 'FirstLine',
|
||||
strSecond = 'SecondLine',
|
||||
link = 'google.com';
|
||||
await editorState.insertTextAtCurrentSelection(strFirst);
|
||||
await tester.pumpAndSettle();
|
||||
await editorState.insertNewLine();
|
||||
await tester.ime.insertText(strSecond);
|
||||
await tester.pumpAndSettle();
|
||||
await editorState.insertTextAtCurrentSelection(strSecond);
|
||||
await tester.pumpAndSettle();
|
||||
final firstLine = find.text(strFirst, findRichText: true),
|
||||
secondLine = find.text(strSecond, findRichText: true);
|
||||
expect(firstLine, findsOneWidget);
|
||||
expect(secondLine, findsOneWidget);
|
||||
|
||||
/// select the first line
|
||||
await tester.doubleTapAt(tester.getCenter(firstLine));
|
||||
await tester.longPress(firstLine);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
/// find aa item and tap it
|
||||
final aaItem = find.byWidgetPredicate(
|
||||
(widget) =>
|
||||
widget is AppFlowyMobileToolbarIconItem &&
|
||||
widget.icon == FlowySvgs.m_toolbar_aa_m,
|
||||
);
|
||||
expect(aaItem, findsOneWidget);
|
||||
await tester.tapButton(aaItem);
|
||||
|
||||
/// find link button and tap it
|
||||
final linkButton = find.byFlowySvg(FlowySvgs.toolbar_link_m);
|
||||
final linkButton = find.byWidgetPredicate(
|
||||
(widget) =>
|
||||
widget is MobileToolbarMenuItemWrapper &&
|
||||
widget.icon == FlowySvgs.m_toolbar_link_m,
|
||||
);
|
||||
expect(linkButton, findsOneWidget);
|
||||
await tester.tapButton(linkButton);
|
||||
|
||||
/// input the link
|
||||
final textFormField = find.byType(TextFormField);
|
||||
expect(textFormField, findsNWidgets(2));
|
||||
final linkField = textFormField.last;
|
||||
await tester.enterText(linkField, testLink);
|
||||
final linkField = find.byWidgetPredicate(
|
||||
(w) =>
|
||||
w is FlowyTextField &&
|
||||
w.hintText == LocaleKeys.document_inlineLink_url_placeholder.tr(),
|
||||
);
|
||||
await tester.enterText(linkField, link);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tapButton(find.byFlowySvg(FlowySvgs.toolbar_link_earth_m));
|
||||
|
||||
/// apply the link
|
||||
/// complete inputting
|
||||
await tester.tapButton(find.text(LocaleKeys.button_done.tr()));
|
||||
|
||||
/// do it again
|
||||
/// select the second line
|
||||
await tester.doubleTapAt(tester.getCenter(secondLine));
|
||||
await tester.longPress(secondLine);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tapButton(aaItem);
|
||||
await tester.tapButton(linkButton);
|
||||
await tester.enterText(linkField, testLink);
|
||||
await tester.enterText(linkField, link);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tapButton(find.byFlowySvg(FlowySvgs.toolbar_link_earth_m));
|
||||
await tester.tapButton(find.text(LocaleKeys.button_done.tr()));
|
||||
|
||||
final firstNode = editorState.getNodeAtPath([0]);
|
||||
final secondNode = editorState.getNodeAtPath([1]);
|
||||
|
||||
Map commonDeltaJson(String insert) => {
|
||||
'insert': insert,
|
||||
'attributes': {'href': testLink, 'is_page_link': false},
|
||||
"insert": insert,
|
||||
"attributes": {"href": link},
|
||||
};
|
||||
|
||||
expect(
|
||||
firstNode?.delta?.toJson(),
|
||||
[commonDeltaJson(strFirst)],
|
||||
commonDeltaJson(strFirst),
|
||||
);
|
||||
expect(
|
||||
secondNode?.delta?.toJson(),
|
||||
[commonDeltaJson(strSecond)],
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('change a link', (tester) async {
|
||||
await tester.launchInAnonymousMode();
|
||||
|
||||
await createNeaPage(tester);
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
final editorState = tester.editor.getCurrentEditorState();
|
||||
const testText = 'TestText';
|
||||
await tester.ime.insertText(testText);
|
||||
final textFinder = find.text(testText, findRichText: true);
|
||||
|
||||
/// select the first line
|
||||
await tester.doubleTapAt(tester.getCenter(textFinder));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
/// find link button and tap it
|
||||
final linkButton = find.byFlowySvg(FlowySvgs.toolbar_link_m);
|
||||
await tester.tapButton(linkButton);
|
||||
|
||||
/// input the link
|
||||
final textFormField = find.byType(TextFormField);
|
||||
expect(textFormField, findsNWidgets(2));
|
||||
final linkField = textFormField.last;
|
||||
await tester.enterText(linkField, testLink);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tapButton(find.byFlowySvg(FlowySvgs.toolbar_link_earth_m));
|
||||
|
||||
/// apply the link
|
||||
await tester.tapButton(find.text(LocaleKeys.button_done.tr()));
|
||||
|
||||
/// show edit link menu
|
||||
await tester.longPress(textFinder);
|
||||
await tester.pumpAndSettle();
|
||||
final linkEditMenu = find.byType(MobileBottomSheetEditLinkWidget);
|
||||
expect(linkEditMenu, findsOneWidget);
|
||||
|
||||
/// remove the link
|
||||
await tester.tapButton(find.byFlowySvg(FlowySvgs.toolbar_link_unlink_m));
|
||||
final node = editorState.getNodeAtPath([0]);
|
||||
expect(
|
||||
node?.delta?.toJson(),
|
||||
[
|
||||
{'insert': testText},
|
||||
],
|
||||
commonDeltaJson(strSecond),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/mobile/presentation/search/mobile_search_ask_ai_entrance.dart';
|
||||
import 'package:appflowy/mobile/presentation/search/mobile_search_result.dart';
|
||||
import 'package:appflowy/mobile/presentation/search/mobile_search_textfield.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import '../../shared/util.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('Search test', () {
|
||||
testWidgets('tap to search page', (tester) async {
|
||||
await tester.launchInAnonymousMode();
|
||||
final searchButton = find.byFlowySvg(FlowySvgs.m_home_search_icon_m);
|
||||
await tester.tapButton(searchButton);
|
||||
|
||||
///check for UI element
|
||||
expect(find.byType(MobileSearchAskAiEntrance), findsNothing);
|
||||
expect(find.byType(MobileSearchRecentList), findsOneWidget);
|
||||
expect(find.byType(MobileSearchResultList), findsNothing);
|
||||
|
||||
/// search for something
|
||||
final searchTextField = find.descendant(
|
||||
of: find.byType(MobileSearchTextfield),
|
||||
matching: find.byType(TextFormField),
|
||||
);
|
||||
final query = '$gettingStarted searching';
|
||||
await tester.enterText(searchTextField, query);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byType(MobileSearchRecentList), findsNothing);
|
||||
expect(find.byType(MobileSearchResultList), findsOneWidget);
|
||||
expect(
|
||||
find.text(LocaleKeys.search_noResultForSearching.tr()),
|
||||
findsOneWidget,
|
||||
);
|
||||
|
||||
/// clear text
|
||||
final clearButton = find.byFlowySvg(FlowySvgs.clear_s);
|
||||
await tester.tapButton(clearButton);
|
||||
expect(find.byType(MobileSearchRecentList), findsOneWidget);
|
||||
expect(find.byType(MobileSearchResultList), findsNothing);
|
||||
|
||||
/// tap cancel button
|
||||
final cancelButton = find.text(LocaleKeys.button_cancel.tr());
|
||||
expect(cancelButton, findsNothing);
|
||||
await tester.enterText(searchTextField, query);
|
||||
await tester.pumpAndSettle();
|
||||
expect(cancelButton, findsOneWidget);
|
||||
await tester.tapButton(cancelButton);
|
||||
expect(cancelButton, findsNothing);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
import 'package:appflowy/ai/ai.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/ai/ai_writer_toolbar_item.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_entities.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import 'util.dart';
|
||||
|
||||
extension AppFlowyAITest on WidgetTester {
|
||||
Future<void> selectAIWriter(AiWriterCommand command) async {
|
||||
await tapButton(find.byType(AiWriterToolbarActionList));
|
||||
await tapButton(find.text(command.i18n));
|
||||
await pumpAndSettle();
|
||||
}
|
||||
|
||||
Future<void> selectModel(String modelName) async {
|
||||
await tapButton(find.byType(SelectModelMenu));
|
||||
await tapButton(find.text(modelName));
|
||||
await pumpAndSettle();
|
||||
}
|
||||
|
||||
Future<void> enterTextInPromptTextField(String text) async {
|
||||
// Wait for the text field to be visible
|
||||
await pumpAndSettle();
|
||||
|
||||
// Find the ExtendedTextField widget
|
||||
final textField = find.descendant(
|
||||
of: find.byType(PromptInputTextField),
|
||||
matching: find.byType(TextField),
|
||||
);
|
||||
expect(textField, findsOneWidget, reason: 'ExtendedTextField not found');
|
||||
|
||||
final widget = element(textField).widget as TextField;
|
||||
expect(widget.enabled, isTrue, reason: 'TextField is not enabled');
|
||||
|
||||
await tap(textField);
|
||||
|
||||
testTextInput.enterText(text);
|
||||
await pumpAndSettle(const Duration(milliseconds: 300));
|
||||
}
|
||||
}
|
||||
@@ -28,8 +28,8 @@ extension AppFlowyAuthTest on WidgetTester {
|
||||
|
||||
await tapButton(find.byType(AccountSignInOutButton));
|
||||
|
||||
expectToSeeText(LocaleKeys.button_yes.tr());
|
||||
await tapButtonWithName(LocaleKeys.button_yes.tr());
|
||||
expectToSeeText(LocaleKeys.button_ok.tr());
|
||||
await tapButtonWithName(LocaleKeys.button_ok.tr());
|
||||
}
|
||||
|
||||
Future<void> tapSignInAsGuest() async {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:appflowy/ai/service/appflowy_ai_service.dart';
|
||||
import 'package:appflowy/env/cloud_env.dart';
|
||||
import 'package:appflowy/env/cloud_env_test.dart';
|
||||
import 'package:appflowy/startup/entry_point.dart';
|
||||
@@ -21,8 +20,6 @@ import 'package:path/path.dart' as p;
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:universal_platform/universal_platform.dart';
|
||||
|
||||
import 'mock/mock_ai.dart';
|
||||
|
||||
class FlowyTestContext {
|
||||
FlowyTestContext({required this.applicationDataDirectory});
|
||||
|
||||
@@ -36,14 +33,13 @@ extension AppFlowyTestBase on WidgetTester {
|
||||
// use to specify the application data directory, if not specified, a temporary directory will be used.
|
||||
String? dataDirectory,
|
||||
Size windowSize = const Size(1600, 1200),
|
||||
String? email,
|
||||
AuthenticatorType? cloudType,
|
||||
AIRepository Function()? aiRepositoryBuilder,
|
||||
String? email,
|
||||
}) async {
|
||||
if (Platform.isLinux || Platform.isWindows || Platform.isMacOS) {
|
||||
// Set the window size
|
||||
await binding.setSurfaceSize(windowSize);
|
||||
}
|
||||
//cloudType = AuthenticatorType.appflowyCloudDevelop;
|
||||
|
||||
mockHotKeyManagerHandlers();
|
||||
final applicationDataDirectory = dataDirectory ??
|
||||
@@ -54,12 +50,38 @@ extension AppFlowyTestBase on WidgetTester {
|
||||
await FlowyRunner.run(
|
||||
AppFlowyApplication(),
|
||||
IntegrationMode.integrationTest,
|
||||
rustEnvsBuilder: () => _buildRustEnvs(cloudType),
|
||||
didInitGetItCallback: () => _initializeCloudServices(
|
||||
cloudType: cloudType,
|
||||
email: email,
|
||||
aiRepositoryBuilder: aiRepositoryBuilder,
|
||||
),
|
||||
rustEnvsBuilder: () {
|
||||
final rustEnvs = <String, String>{};
|
||||
if (cloudType != null) {
|
||||
switch (cloudType) {
|
||||
case AuthenticatorType.appflowyCloudSelfHost:
|
||||
rustEnvs["GOTRUE_ADMIN_EMAIL"] = "admin@example.com";
|
||||
rustEnvs["GOTRUE_ADMIN_PASSWORD"] = "password";
|
||||
break;
|
||||
default:
|
||||
throw Exception("not supported");
|
||||
}
|
||||
}
|
||||
return rustEnvs;
|
||||
},
|
||||
didInitGetItCallback: () {
|
||||
return Future(
|
||||
() async {
|
||||
if (cloudType != null) {
|
||||
switch (cloudType) {
|
||||
case AuthenticatorType.appflowyCloudSelfHost:
|
||||
await useTestSelfHostedAppFlowyCloud();
|
||||
getIt.unregister<AuthService>();
|
||||
getIt.registerFactory<AuthService>(
|
||||
() => AppFlowyCloudMockAuthService(email: email),
|
||||
);
|
||||
default:
|
||||
throw Exception("not supported");
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
await waitUntilSignInPageShow();
|
||||
@@ -68,76 +90,6 @@ extension AppFlowyTestBase on WidgetTester {
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, String> _buildRustEnvs(AuthenticatorType? cloudType) {
|
||||
final rustEnvs = <String, String>{};
|
||||
if (cloudType != null) {
|
||||
switch (cloudType) {
|
||||
case AuthenticatorType.local:
|
||||
break;
|
||||
case AuthenticatorType.appflowyCloudSelfHost:
|
||||
case AuthenticatorType.appflowyCloudDevelop:
|
||||
rustEnvs["GOTRUE_ADMIN_EMAIL"] = "admin@example.com";
|
||||
rustEnvs["GOTRUE_ADMIN_PASSWORD"] = "password";
|
||||
break;
|
||||
default:
|
||||
throw Exception("Unsupported cloud type: $cloudType");
|
||||
}
|
||||
}
|
||||
return rustEnvs;
|
||||
}
|
||||
|
||||
Future<void> _initializeCloudServices({
|
||||
required AuthenticatorType? cloudType,
|
||||
String? email,
|
||||
AIRepository Function()? aiRepositoryBuilder,
|
||||
}) async {
|
||||
if (cloudType == null) return;
|
||||
|
||||
switch (cloudType) {
|
||||
case AuthenticatorType.local:
|
||||
await useLocalServer();
|
||||
break;
|
||||
case AuthenticatorType.appflowyCloudSelfHost:
|
||||
await _setupAppFlowyCloud(
|
||||
useLocal: false,
|
||||
email: email,
|
||||
aiRepositoryBuilder: aiRepositoryBuilder,
|
||||
);
|
||||
break;
|
||||
case AuthenticatorType.appflowyCloudDevelop:
|
||||
await _setupAppFlowyCloud(
|
||||
useLocal: integrationMode().isDevelop,
|
||||
email: email,
|
||||
aiRepositoryBuilder: aiRepositoryBuilder,
|
||||
);
|
||||
break;
|
||||
default:
|
||||
throw Exception("Unsupported cloud type: $cloudType");
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _setupAppFlowyCloud({
|
||||
required bool useLocal,
|
||||
String? email,
|
||||
AIRepository Function()? aiRepositoryBuilder,
|
||||
}) async {
|
||||
if (useLocal) {
|
||||
await useAppFlowyCloudDevelop("http://localhost");
|
||||
} else {
|
||||
await useSelfHostedAppFlowyCloud(TestEnv.afCloudUrl);
|
||||
}
|
||||
|
||||
getIt.unregister<AuthService>();
|
||||
getIt.unregister<AIRepository>();
|
||||
|
||||
getIt.registerFactory<AuthService>(
|
||||
() => AppFlowyCloudMockAuthService(email: email),
|
||||
);
|
||||
getIt.registerFactory<AIRepository>(
|
||||
aiRepositoryBuilder ?? () => MockAIRepository(),
|
||||
);
|
||||
}
|
||||
|
||||
void mockHotKeyManagerHandlers() {
|
||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||
.setMockMethodCallHandler(const MethodChannel('hotkey_manager'),
|
||||
@@ -318,6 +270,10 @@ extension AppFlowyFinderTestBase on CommonFinders {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> useTestSelfHostedAppFlowyCloud() async {
|
||||
await useSelfHostedAppFlowyCloudWithURL(TestEnv.afCloudUrl);
|
||||
}
|
||||
|
||||
Future<String> mockApplicationDataStorage({
|
||||
// use to append after the application data directory
|
||||
String? pathExtension,
|
||||
|
||||
@@ -67,10 +67,12 @@ extension CommonOperations on WidgetTester {
|
||||
} else {
|
||||
// cloud version
|
||||
final anonymousButton = find.byType(SignInAnonymousButtonV2);
|
||||
await tapButton(anonymousButton, warnIfMissed: true);
|
||||
await tapButton(anonymousButton);
|
||||
}
|
||||
|
||||
await pumpAndSettle(const Duration(milliseconds: 200));
|
||||
if (Platform.isWindows) {
|
||||
await pumpAndSettle(const Duration(milliseconds: 200));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> tapContinousAnotherWay() async {
|
||||
|
||||
@@ -1,128 +0,0 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:appflowy/ai/service/ai_entities.dart';
|
||||
import 'package:appflowy/ai/service/appflowy_ai_service.dart';
|
||||
import 'package:appflowy/ai/service/error.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_entities.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-ai/entities.pbenum.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
|
||||
final _mockAiMap = <CompletionTypePB, Map<String, List<String>>>{
|
||||
CompletionTypePB.ImproveWriting: {
|
||||
"I have an apple": [
|
||||
"I",
|
||||
"have",
|
||||
"an",
|
||||
"apple",
|
||||
"and",
|
||||
"a",
|
||||
"banana",
|
||||
],
|
||||
},
|
||||
CompletionTypePB.SpellingAndGrammar: {
|
||||
"We didn’t had enough money": [
|
||||
"We",
|
||||
"didn’t",
|
||||
"have",
|
||||
"enough",
|
||||
"money",
|
||||
],
|
||||
},
|
||||
CompletionTypePB.UserQuestion: {
|
||||
"Explain the concept of TPU": [
|
||||
"TPU",
|
||||
"is",
|
||||
"a",
|
||||
"tensor",
|
||||
"processing",
|
||||
"unit",
|
||||
"that",
|
||||
"is",
|
||||
"designed",
|
||||
"to",
|
||||
"accelerate",
|
||||
"machine",
|
||||
],
|
||||
"How about GPU?": [
|
||||
"GPU",
|
||||
"is",
|
||||
"a",
|
||||
"graphics",
|
||||
"processing",
|
||||
"unit",
|
||||
"that",
|
||||
"is",
|
||||
"designed",
|
||||
"to",
|
||||
"accelerate",
|
||||
"machine",
|
||||
"learning",
|
||||
"tasks",
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
abstract class StreamCompletionValidator {
|
||||
bool validate(
|
||||
String text,
|
||||
String? objectId,
|
||||
CompletionTypePB completionType,
|
||||
PredefinedFormat? format,
|
||||
List<String> sourceIds,
|
||||
List<AiWriterRecord> history,
|
||||
);
|
||||
}
|
||||
|
||||
class MockCompletionStream extends Mock implements CompletionStream {}
|
||||
|
||||
class MockAIRepository extends Mock implements AppFlowyAIService {
|
||||
MockAIRepository({this.validator});
|
||||
StreamCompletionValidator? validator;
|
||||
|
||||
@override
|
||||
Future<(String, CompletionStream)?> streamCompletion({
|
||||
String? objectId,
|
||||
required String text,
|
||||
PredefinedFormat? format,
|
||||
List<String> sourceIds = const [],
|
||||
List<AiWriterRecord> history = const [],
|
||||
required CompletionTypePB completionType,
|
||||
required Future<void> Function() onStart,
|
||||
required Future<void> Function(String text) processMessage,
|
||||
required Future<void> Function(String text) processAssistMessage,
|
||||
required Future<void> Function() onEnd,
|
||||
required void Function(AIError error) onError,
|
||||
required void Function(LocalAIStreamingState state)
|
||||
onLocalAIStreamingStateChange,
|
||||
}) async {
|
||||
if (validator != null) {
|
||||
if (!validator!.validate(
|
||||
text,
|
||||
objectId,
|
||||
completionType,
|
||||
format,
|
||||
sourceIds,
|
||||
history,
|
||||
)) {
|
||||
throw Exception('Invalid completion');
|
||||
}
|
||||
}
|
||||
final stream = MockCompletionStream();
|
||||
unawaited(
|
||||
Future(() async {
|
||||
await onStart();
|
||||
final lines = _mockAiMap[completionType]?[text.trim()];
|
||||
|
||||
if (lines == null) {
|
||||
throw Exception('No mock ai found for $text and $completionType');
|
||||
}
|
||||
|
||||
for (final line in lines) {
|
||||
await processMessage('$line ');
|
||||
}
|
||||
await onEnd();
|
||||
}),
|
||||
);
|
||||
return ('mock_id', stream);
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import 'package:appflowy/workspace/presentation/settings/settings_dialog.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/settings_menu_element.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text_field.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
@@ -78,14 +79,14 @@ extension AppFlowySettings on WidgetTester {
|
||||
// Enable editing username
|
||||
final editUsernameFinder = find.descendant(
|
||||
of: find.byType(AccountUserProfile),
|
||||
matching: find.byFlowySvg(FlowySvgs.toolbar_link_edit_m),
|
||||
matching: find.byFlowySvg(FlowySvgs.edit_s),
|
||||
);
|
||||
await tap(editUsernameFinder, warnIfMissed: false);
|
||||
await pumpAndSettle();
|
||||
|
||||
final userNameFinder = find.descendant(
|
||||
of: find.byType(AccountUserProfile),
|
||||
matching: find.byType(TextField),
|
||||
matching: find.byType(FlowyTextField),
|
||||
);
|
||||
await enterText(userNameFinder, name);
|
||||
await pumpAndSettle();
|
||||
|
||||
@@ -8,35 +8,35 @@ PODS:
|
||||
- ReachabilitySwift
|
||||
- device_info_plus (0.0.1):
|
||||
- Flutter
|
||||
- DKImagePickerController/Core (4.3.9):
|
||||
- DKImagePickerController/Core (4.3.4):
|
||||
- DKImagePickerController/ImageDataManager
|
||||
- DKImagePickerController/Resource
|
||||
- DKImagePickerController/ImageDataManager (4.3.9)
|
||||
- DKImagePickerController/PhotoGallery (4.3.9):
|
||||
- DKImagePickerController/ImageDataManager (4.3.4)
|
||||
- DKImagePickerController/PhotoGallery (4.3.4):
|
||||
- DKImagePickerController/Core
|
||||
- DKPhotoGallery
|
||||
- DKImagePickerController/Resource (4.3.9)
|
||||
- DKPhotoGallery (0.0.19):
|
||||
- DKPhotoGallery/Core (= 0.0.19)
|
||||
- DKPhotoGallery/Model (= 0.0.19)
|
||||
- DKPhotoGallery/Preview (= 0.0.19)
|
||||
- DKPhotoGallery/Resource (= 0.0.19)
|
||||
- DKImagePickerController/Resource (4.3.4)
|
||||
- DKPhotoGallery (0.0.17):
|
||||
- DKPhotoGallery/Core (= 0.0.17)
|
||||
- DKPhotoGallery/Model (= 0.0.17)
|
||||
- DKPhotoGallery/Preview (= 0.0.17)
|
||||
- DKPhotoGallery/Resource (= 0.0.17)
|
||||
- SDWebImage
|
||||
- SwiftyGif
|
||||
- DKPhotoGallery/Core (0.0.19):
|
||||
- DKPhotoGallery/Core (0.0.17):
|
||||
- DKPhotoGallery/Model
|
||||
- DKPhotoGallery/Preview
|
||||
- SDWebImage
|
||||
- SwiftyGif
|
||||
- DKPhotoGallery/Model (0.0.19):
|
||||
- DKPhotoGallery/Model (0.0.17):
|
||||
- SDWebImage
|
||||
- SwiftyGif
|
||||
- DKPhotoGallery/Preview (0.0.19):
|
||||
- DKPhotoGallery/Preview (0.0.17):
|
||||
- DKPhotoGallery/Model
|
||||
- DKPhotoGallery/Resource
|
||||
- SDWebImage
|
||||
- SwiftyGif
|
||||
- DKPhotoGallery/Resource (0.0.19):
|
||||
- DKPhotoGallery/Resource (0.0.17):
|
||||
- SDWebImage
|
||||
- SwiftyGif
|
||||
- file_picker (0.0.1):
|
||||
@@ -65,17 +65,17 @@ PODS:
|
||||
- FlutterMacOS
|
||||
- permission_handler_apple (9.3.0):
|
||||
- Flutter
|
||||
- ReachabilitySwift (5.2.4)
|
||||
- ReachabilitySwift (5.0.0)
|
||||
- saver_gallery (0.0.1):
|
||||
- Flutter
|
||||
- SDWebImage (5.21.0):
|
||||
- SDWebImage/Core (= 5.21.0)
|
||||
- SDWebImage/Core (5.21.0)
|
||||
- Sentry/HybridSDK (8.46.0)
|
||||
- sentry_flutter (8.14.2):
|
||||
- SDWebImage (5.14.2):
|
||||
- SDWebImage/Core (= 5.14.2)
|
||||
- SDWebImage/Core (5.14.2)
|
||||
- Sentry/HybridSDK (8.35.1)
|
||||
- sentry_flutter (8.8.0):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- Sentry/HybridSDK (= 8.46.0)
|
||||
- Sentry/HybridSDK (= 8.35.1)
|
||||
- share_plus (0.0.1):
|
||||
- Flutter
|
||||
- shared_preferences_foundation (0.0.1):
|
||||
@@ -86,8 +86,8 @@ PODS:
|
||||
- FlutterMacOS
|
||||
- super_native_extensions (0.0.1):
|
||||
- Flutter
|
||||
- SwiftyGif (5.4.5)
|
||||
- Toast (4.1.1)
|
||||
- SwiftyGif (5.4.3)
|
||||
- Toast (4.0.0)
|
||||
- url_launcher_ios (0.0.1):
|
||||
- Flutter
|
||||
- webview_flutter_wkwebview (0.0.1):
|
||||
@@ -181,37 +181,37 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/webview_flutter_wkwebview/darwin"
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
app_links: 3da4c36b46cac3bf24eb897f1a6ce80bda109874
|
||||
appflowy_backend: 78f6a053f756e6bc29bcc5a2106cbe77b756e97a
|
||||
connectivity_plus: 481668c94744c30c53b8895afb39159d1e619bdf
|
||||
device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe
|
||||
DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
|
||||
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
|
||||
file_picker: 9b3292d7c8bc68c8a7bf8eb78f730e49c8efc517
|
||||
flowy_infra_ui: 931b73a18b54a392ab6152eebe29a63a30751f53
|
||||
app_links: e7a6750a915a9e161c58d91bc610e8cd1d4d0ad0
|
||||
appflowy_backend: 144c20d8bfb298c4e10fa3fa6701a9f41bf98b88
|
||||
connectivity_plus: bf0076dd84a130856aa636df1c71ccaff908fa1d
|
||||
device_info_plus: 97af1d7e84681a90d0693e63169a5d50e0839a0d
|
||||
DKImagePickerController: b512c28220a2b8ac7419f21c491fc8534b7601ac
|
||||
DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179
|
||||
file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655
|
||||
flowy_infra_ui: 0455e1fa8c51885aa1437848e361e99419f34ebc
|
||||
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
|
||||
fluttertoast: 76fea30fcf04176325f6864c87306927bd7d2038
|
||||
image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a
|
||||
integration_test: 4a889634ef21a45d28d50d622cf412dc6d9f586e
|
||||
irondash_engine_context: 8e58ca8e0212ee9d1c7dc6a42121849986c88486
|
||||
keyboard_height_plugin: ef70a8181b29f27670e9e2450814ca6b6dc05b05
|
||||
open_filex: 432f3cd11432da3e39f47fcc0df2b1603854eff1
|
||||
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
|
||||
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
|
||||
permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d
|
||||
ReachabilitySwift: 32793e867593cfc1177f5d16491e3a197d2fccda
|
||||
saver_gallery: af2d0c762dafda254e0ad025ef0dabd6506cd490
|
||||
SDWebImage: f84b0feeb08d2d11e6a9b843cb06d75ebf5b8868
|
||||
Sentry: da60d980b197a46db0b35ea12cb8f39af48d8854
|
||||
sentry_flutter: 27892878729f42701297c628eb90e7c6529f3684
|
||||
share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a
|
||||
shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7
|
||||
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
|
||||
super_native_extensions: b763c02dc3a8fd078389f410bf15149179020cb4
|
||||
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
|
||||
Toast: 1f5ea13423a1e6674c4abdac5be53587ae481c4e
|
||||
url_launcher_ios: 694010445543906933d732453a59da0a173ae33d
|
||||
webview_flutter_wkwebview: 44d4dee7d7056d5ad185d25b38404436d56c547c
|
||||
fluttertoast: e9a18c7be5413da53898f660530c56f35edfba9c
|
||||
image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1
|
||||
integration_test: 252f60fa39af5e17c3aa9899d35d908a0721b573
|
||||
irondash_engine_context: 3458bf979b90d616ffb8ae03a150bafe2e860cc9
|
||||
keyboard_height_plugin: 43fa8bba20fd5c4fdeed5076466b8b9d43cc6b86
|
||||
open_filex: 6e26e659846ec990262224a12ef1c528bb4edbe4
|
||||
package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4
|
||||
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
|
||||
permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2
|
||||
ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825
|
||||
saver_gallery: 76172dc4bf6b40e66d694948ada9ff402304dd87
|
||||
SDWebImage: b9a731e1d6307f44ca703b3976d18c24ca561e84
|
||||
Sentry: 1fe34e9c2cbba1e347623610d26db121dcb569f1
|
||||
sentry_flutter: a39c2a2d67d5e5b9cb0b94a4985c76dd5b3fc737
|
||||
share_plus: 8b6f8b3447e494cca5317c8c3073de39b3600d1f
|
||||
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
|
||||
sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d
|
||||
super_native_extensions: 4916b3c627a9c7fffdc48a23a9eca0b1ac228fa7
|
||||
SwiftyGif: 6c3eafd0ce693cad58bb63d2b2fb9bacb8552780
|
||||
Toast: 91b396c56ee72a5790816f40d3a94dd357abc196
|
||||
url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe
|
||||
webview_flutter_wkwebview: 0982481e3d9c78fd5c6f62a002fcd24fc791f1e4
|
||||
|
||||
PODFILE CHECKSUM: d0d9b4ff572d8695c38eb3f9b490f55cdfc57eca
|
||||
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
export 'service/ai_entities.dart';
|
||||
export 'service/ai_prompt_input_bloc.dart';
|
||||
export 'service/ai_prompt_selector_cubit.dart';
|
||||
export 'service/appflowy_ai_service.dart';
|
||||
export 'service/error.dart';
|
||||
export 'service/ai_model_state_notifier.dart';
|
||||
export 'service/select_model_bloc.dart';
|
||||
export 'widgets/loading_indicator.dart';
|
||||
export 'widgets/prompt_input/action_buttons.dart';
|
||||
export 'widgets/prompt_input/desktop_prompt_input.dart';
|
||||
export 'widgets/prompt_input/desktop_prompt_text_field.dart';
|
||||
export 'widgets/prompt_input/file_attachment_list.dart';
|
||||
export 'widgets/prompt_input/layout_define.dart';
|
||||
export 'widgets/prompt_input/mention_page_bottom_sheet.dart';
|
||||
export 'widgets/prompt_input/mention_page_menu.dart';
|
||||
export 'widgets/prompt_input/prompt_input_text_controller.dart';
|
||||
export 'widgets/prompt_input/mentioned_page_text_span.dart';
|
||||
export 'widgets/prompt_input/predefined_format_buttons.dart';
|
||||
export 'widgets/prompt_input/select_sources_bottom_sheet.dart';
|
||||
export 'widgets/prompt_input/select_sources_menu.dart';
|
||||
|
||||
@@ -3,9 +3,6 @@ import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-ai/protobuf.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
part 'ai_entities.g.dart';
|
||||
|
||||
class AIStreamEventPrefix {
|
||||
static const data = 'data:';
|
||||
@@ -108,110 +105,3 @@ enum TextFormat {
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
enum AiPromptCategory {
|
||||
@JsonValue("other")
|
||||
other,
|
||||
@JsonValue("development")
|
||||
development,
|
||||
@JsonValue("writing")
|
||||
writing,
|
||||
@JsonValue("healthAndFitness")
|
||||
healthAndFitness,
|
||||
@JsonValue("business")
|
||||
business,
|
||||
@JsonValue("marketing")
|
||||
marketing,
|
||||
@JsonValue("travel")
|
||||
travel,
|
||||
@JsonValue("contentSeo")
|
||||
contentSeo,
|
||||
@JsonValue("emailMarketing")
|
||||
emailMarketing,
|
||||
@JsonValue("paidAds")
|
||||
paidAds,
|
||||
@JsonValue("prCommunication")
|
||||
prCommunication,
|
||||
@JsonValue("recruiting")
|
||||
recruiting,
|
||||
@JsonValue("sales")
|
||||
sales,
|
||||
@JsonValue("socialMedia")
|
||||
socialMedia,
|
||||
@JsonValue("strategy")
|
||||
strategy,
|
||||
@JsonValue("caseStudies")
|
||||
caseStudies,
|
||||
@JsonValue("salesCopy")
|
||||
salesCopy,
|
||||
@JsonValue("education")
|
||||
education,
|
||||
@JsonValue("work")
|
||||
work,
|
||||
@JsonValue("podcastProduction")
|
||||
podcastProduction,
|
||||
@JsonValue("copyWriting")
|
||||
copyWriting,
|
||||
@JsonValue("customerSuccess")
|
||||
customerSuccess;
|
||||
|
||||
String get i18n {
|
||||
return switch (this) {
|
||||
other => LocaleKeys.ai_customPrompt_others.tr(),
|
||||
development => LocaleKeys.ai_customPrompt_development.tr(),
|
||||
writing => LocaleKeys.ai_customPrompt_writing.tr(),
|
||||
healthAndFitness => LocaleKeys.ai_customPrompt_healthAndFitness.tr(),
|
||||
business => LocaleKeys.ai_customPrompt_business.tr(),
|
||||
marketing => LocaleKeys.ai_customPrompt_marketing.tr(),
|
||||
travel => LocaleKeys.ai_customPrompt_travel.tr(),
|
||||
contentSeo => LocaleKeys.ai_customPrompt_contentSeo.tr(),
|
||||
emailMarketing => LocaleKeys.ai_customPrompt_emailMarketing.tr(),
|
||||
paidAds => LocaleKeys.ai_customPrompt_paidAds.tr(),
|
||||
prCommunication => LocaleKeys.ai_customPrompt_prCommunication.tr(),
|
||||
recruiting => LocaleKeys.ai_customPrompt_recruiting.tr(),
|
||||
sales => LocaleKeys.ai_customPrompt_sales.tr(),
|
||||
socialMedia => LocaleKeys.ai_customPrompt_socialMedia.tr(),
|
||||
strategy => LocaleKeys.ai_customPrompt_strategy.tr(),
|
||||
caseStudies => LocaleKeys.ai_customPrompt_caseStudies.tr(),
|
||||
salesCopy => LocaleKeys.ai_customPrompt_salesCopy.tr(),
|
||||
education => LocaleKeys.ai_customPrompt_education.tr(),
|
||||
work => LocaleKeys.ai_customPrompt_work.tr(),
|
||||
podcastProduction => LocaleKeys.ai_customPrompt_podcastProduction.tr(),
|
||||
copyWriting => LocaleKeys.ai_customPrompt_copyWriting.tr(),
|
||||
customerSuccess => LocaleKeys.ai_customPrompt_customerSuccess.tr(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@JsonSerializable()
|
||||
class AiPrompt extends Equatable {
|
||||
const AiPrompt({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.content,
|
||||
required this.category,
|
||||
required this.example,
|
||||
required this.isFeatured,
|
||||
});
|
||||
|
||||
factory AiPrompt.fromJson(Map<String, dynamic> json) =>
|
||||
_$AiPromptFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$AiPromptToJson(this);
|
||||
|
||||
final String id;
|
||||
final String name;
|
||||
final String content;
|
||||
@JsonKey(
|
||||
unknownEnumValue: AiPromptCategory.other,
|
||||
defaultValue: AiPromptCategory.other,
|
||||
)
|
||||
final AiPromptCategory category;
|
||||
@JsonKey(defaultValue: "")
|
||||
final String example;
|
||||
@JsonKey(defaultValue: false)
|
||||
final bool isFeatured;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [id, name, content, category, example, isFeatured];
|
||||
}
|
||||
@@ -7,38 +7,15 @@ import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-ai/entities.pb.dart';
|
||||
import 'package:appflowy_result/appflowy_result.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:protobuf/protobuf.dart';
|
||||
import 'package:universal_platform/universal_platform.dart';
|
||||
|
||||
typedef OnModelStateChangedCallback = void Function(AIModelState state);
|
||||
typedef OnModelStateChangedCallback = void Function(AiType, bool, String);
|
||||
typedef OnAvailableModelsChangedCallback = void Function(
|
||||
List<AIModelPB>,
|
||||
AIModelPB?,
|
||||
);
|
||||
|
||||
/// Represents the state of an AI model
|
||||
class AIModelState {
|
||||
const AIModelState({
|
||||
required this.type,
|
||||
required this.hintText,
|
||||
required this.tooltip,
|
||||
required this.isEditable,
|
||||
required this.localAIEnabled,
|
||||
});
|
||||
final AiType type;
|
||||
|
||||
/// The text displayed as placeholder/hint in the input field
|
||||
/// Shows different messages based on AI state (enabled, initializing, disabled)
|
||||
final String hintText;
|
||||
|
||||
/// Optional tooltip text that appears on hover
|
||||
/// Provides additional context about the current state of the AI
|
||||
/// Null when no tooltip should be shown
|
||||
final String? tooltip;
|
||||
|
||||
final bool isEditable;
|
||||
final bool localAIEnabled;
|
||||
}
|
||||
|
||||
class AIModelStateNotifier {
|
||||
AIModelStateNotifier({required this.objectId})
|
||||
: _localAIListener =
|
||||
@@ -51,50 +28,51 @@ class AIModelStateNotifier {
|
||||
final String objectId;
|
||||
final LocalAIStateListener? _localAIListener;
|
||||
final AIModelSwitchListener _aiModelSwitchListener;
|
||||
|
||||
LocalAIPB? _localAIState;
|
||||
ModelSelectionPB? _modelSelection;
|
||||
|
||||
AIModelState _currentState = _defaultState();
|
||||
List<AIModelPB> _availableModels = [];
|
||||
AIModelPB? _selectedModel;
|
||||
AvailableModelsPB? _availableModels;
|
||||
|
||||
// callbacks
|
||||
final List<OnModelStateChangedCallback> _stateChangedCallbacks = [];
|
||||
final List<OnAvailableModelsChangedCallback>
|
||||
_availableModelsChangedCallbacks = [];
|
||||
|
||||
/// Starts platform-specific listeners
|
||||
void _startListening() {
|
||||
if (UniversalPlatform.isDesktop) {
|
||||
_localAIListener?.start(
|
||||
stateCallback: (state) async {
|
||||
_localAIState = state;
|
||||
_updateAll();
|
||||
_notifyStateChanged();
|
||||
|
||||
if (state.state == RunningStatePB.Running ||
|
||||
state.state == RunningStatePB.Stopped) {
|
||||
await _loadAvailableModels();
|
||||
_notifyAvailableModelsChanged();
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
_aiModelSwitchListener.start(
|
||||
onUpdateSelectedModel: (model) async {
|
||||
_selectedModel = model;
|
||||
_updateAll();
|
||||
final updatedModels = _availableModels?.deepCopy()
|
||||
?..selectedModel = model;
|
||||
_availableModels = updatedModels;
|
||||
_notifyAvailableModelsChanged();
|
||||
|
||||
if (model.isLocal && UniversalPlatform.isDesktop) {
|
||||
await _loadLocalState();
|
||||
_updateAll();
|
||||
await _loadLocalAiState();
|
||||
}
|
||||
_notifyStateChanged();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _init() async {
|
||||
await Future.wait([
|
||||
if (UniversalPlatform.isDesktop) _loadLocalState(),
|
||||
_loadModelSelection(),
|
||||
]);
|
||||
_updateAll();
|
||||
void _init() async {
|
||||
await Future.wait([_loadLocalAiState(), _loadAvailableModels()]);
|
||||
_notifyStateChanged();
|
||||
_notifyAvailableModelsChanged();
|
||||
}
|
||||
|
||||
/// Register callbacks for state or available-models changes
|
||||
void addListener({
|
||||
OnModelStateChangedCallback? onStateChanged,
|
||||
OnAvailableModelsChangedCallback? onAvailableModelsChanged,
|
||||
@@ -107,7 +85,6 @@ class AIModelStateNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove previously registered callbacks
|
||||
void removeListener({
|
||||
OnModelStateChangedCallback? onStateChanged,
|
||||
OnAvailableModelsChangedCallback? onAvailableModelsChanged,
|
||||
@@ -127,88 +104,78 @@ class AIModelStateNotifier {
|
||||
await _aiModelSwitchListener.stop();
|
||||
}
|
||||
|
||||
/// Returns current AIModelState
|
||||
AIModelState getState() => _currentState;
|
||||
|
||||
/// Returns available models and the selected model
|
||||
(List<AIModelPB>, AIModelPB?) getModelSelection() =>
|
||||
(_availableModels, _selectedModel);
|
||||
|
||||
void _updateAll() {
|
||||
_currentState = _computeState();
|
||||
for (final cb in _stateChangedCallbacks) {
|
||||
cb(_currentState);
|
||||
(AiType, String, bool) getState() {
|
||||
if (UniversalPlatform.isMobile) {
|
||||
return (AiType.cloud, LocaleKeys.chat_inputMessageHint.tr(), true);
|
||||
}
|
||||
for (final cb in _availableModelsChangedCallbacks) {
|
||||
cb(_availableModels, _selectedModel);
|
||||
|
||||
final availableModels = _availableModels;
|
||||
final localAiState = _localAIState;
|
||||
|
||||
if (availableModels == null) {
|
||||
return (AiType.cloud, LocaleKeys.chat_inputMessageHint.tr(), true);
|
||||
}
|
||||
if (localAiState == null) {
|
||||
Log.warn("Cannot get local AI state");
|
||||
return (AiType.cloud, LocaleKeys.chat_inputMessageHint.tr(), true);
|
||||
}
|
||||
|
||||
if (!availableModels.selectedModel.isLocal) {
|
||||
return (AiType.cloud, LocaleKeys.chat_inputMessageHint.tr(), true);
|
||||
}
|
||||
|
||||
final editable = localAiState.state == RunningStatePB.Running;
|
||||
final hintText = editable
|
||||
? LocaleKeys.chat_inputLocalAIMessageHint.tr()
|
||||
: LocaleKeys.settings_aiPage_keys_localAIInitializing.tr();
|
||||
|
||||
return (AiType.local, hintText, editable);
|
||||
}
|
||||
|
||||
(List<AIModelPB>, AIModelPB?) getAvailableModels() {
|
||||
final availableModels = _availableModels;
|
||||
if (availableModels == null) {
|
||||
return ([], null);
|
||||
}
|
||||
return (availableModels.models, availableModels.selectedModel);
|
||||
}
|
||||
|
||||
void _notifyAvailableModelsChanged() {
|
||||
final (models, selectedModel) = getAvailableModels();
|
||||
for (final callback in _availableModelsChangedCallbacks) {
|
||||
callback(models, selectedModel);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _loadModelSelection() async {
|
||||
await AIEventGetSourceModelSelection(
|
||||
ModelSourcePB(source: objectId),
|
||||
).send().fold(
|
||||
(ms) {
|
||||
_modelSelection = ms;
|
||||
_availableModels = ms.models;
|
||||
_selectedModel = ms.selectedModel;
|
||||
},
|
||||
(e) => Log.error("Failed to fetch models: \$e"),
|
||||
);
|
||||
void _notifyStateChanged() {
|
||||
final (type, hintText, isEditable) = getState();
|
||||
for (final callback in _stateChangedCallbacks) {
|
||||
callback(type, isEditable, hintText);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _loadLocalState() async {
|
||||
await AIEventGetLocalAIState().send().fold(
|
||||
(s) => _localAIState = s,
|
||||
(e) => Log.error("Failed to fetch local AI state: \$e"),
|
||||
Future<void> _loadAvailableModels() {
|
||||
final payload = AvailableModelsQueryPB(source: objectId);
|
||||
return AIEventGetAvailableModels(payload).send().fold(
|
||||
(models) => _availableModels = models,
|
||||
(err) => Log.error("Failed to get available models: $err"),
|
||||
);
|
||||
}
|
||||
|
||||
static AIModelState _defaultState() => AIModelState(
|
||||
type: AiType.cloud,
|
||||
hintText: LocaleKeys.chat_inputMessageHint.tr(),
|
||||
tooltip: null,
|
||||
isEditable: true,
|
||||
localAIEnabled: false,
|
||||
);
|
||||
|
||||
/// Core logic computing the state from local and selection data
|
||||
AIModelState _computeState() {
|
||||
if (UniversalPlatform.isMobile) return _defaultState();
|
||||
|
||||
if (_modelSelection == null || _localAIState == null) {
|
||||
return _defaultState();
|
||||
}
|
||||
|
||||
if (!_selectedModel!.isLocal) {
|
||||
return _defaultState();
|
||||
}
|
||||
|
||||
final enabled = _localAIState!.enabled;
|
||||
final running = _localAIState!.isReady;
|
||||
final hintKey = enabled
|
||||
? (running
|
||||
? LocaleKeys.chat_inputLocalAIMessageHint
|
||||
: LocaleKeys.settings_aiPage_keys_localAIInitializing)
|
||||
: LocaleKeys.settings_aiPage_keys_localAIDisabled;
|
||||
final tooltipKey = enabled
|
||||
? (running
|
||||
? null
|
||||
: LocaleKeys.settings_aiPage_keys_localAINotReadyTextFieldPrompt)
|
||||
: LocaleKeys.settings_aiPage_keys_localAIDisabledTextFieldPrompt;
|
||||
|
||||
return AIModelState(
|
||||
type: AiType.local,
|
||||
hintText: hintKey.tr(),
|
||||
tooltip: tooltipKey?.tr(),
|
||||
isEditable: running,
|
||||
localAIEnabled: enabled,
|
||||
);
|
||||
Future<void> _loadLocalAiState() {
|
||||
return AIEventGetLocalAIState().send().fold(
|
||||
(localAIState) => _localAIState = localAIState,
|
||||
(error) => Log.error("Failed to get local AI state: $error"),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
extension AIModelPBExtension on AIModelPB {
|
||||
bool get isDefault => name == 'Auto';
|
||||
String get i18n =>
|
||||
isDefault ? LocaleKeys.chat_switchModel_autoModel.tr() : name;
|
||||
extension AiModelExtension on AIModelPB {
|
||||
bool get isDefault {
|
||||
return name == "Auto";
|
||||
}
|
||||
|
||||
String get i18n {
|
||||
return isDefault ? LocaleKeys.chat_switchModel_autoModel.tr() : name;
|
||||
}
|
||||
}
|
||||
@@ -33,10 +33,12 @@ class AIPromptInputBloc extends Bloc<AIPromptInputEvent, AIPromptInputState> {
|
||||
on<AIPromptInputEvent>(
|
||||
(event, emit) {
|
||||
event.when(
|
||||
updateAIState: (modelState) {
|
||||
updateAIState: (aiType, editable, hintText) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
modelState: modelState,
|
||||
aiType: aiType,
|
||||
editable: editable,
|
||||
hintText: hintText,
|
||||
),
|
||||
);
|
||||
},
|
||||
@@ -103,19 +105,15 @@ class AIPromptInputBloc extends Bloc<AIPromptInputEvent, AIPromptInputState> {
|
||||
|
||||
void _startListening() {
|
||||
aiModelStateNotifier.addListener(
|
||||
onStateChanged: (modelState) {
|
||||
add(
|
||||
AIPromptInputEvent.updateAIState(modelState),
|
||||
);
|
||||
onStateChanged: (aiType, editable, hintText) {
|
||||
add(AIPromptInputEvent.updateAIState(aiType, editable, hintText));
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _init() {
|
||||
final modelState = aiModelStateNotifier.getState();
|
||||
add(
|
||||
AIPromptInputEvent.updateAIState(modelState),
|
||||
);
|
||||
final (aiType, hintText, isEditable) = aiModelStateNotifier.getState();
|
||||
add(AIPromptInputEvent.updateAIState(aiType, isEditable, hintText));
|
||||
}
|
||||
|
||||
Map<String, dynamic> consumeMetadata() {
|
||||
@@ -135,7 +133,9 @@ class AIPromptInputBloc extends Bloc<AIPromptInputEvent, AIPromptInputState> {
|
||||
@freezed
|
||||
class AIPromptInputEvent with _$AIPromptInputEvent {
|
||||
const factory AIPromptInputEvent.updateAIState(
|
||||
AIModelState modelState,
|
||||
AiType aiType,
|
||||
bool editable,
|
||||
String hintText,
|
||||
) = _UpdateAIState;
|
||||
|
||||
const factory AIPromptInputEvent.toggleShowPredefinedFormat() =
|
||||
@@ -156,27 +156,25 @@ class AIPromptInputEvent with _$AIPromptInputEvent {
|
||||
@freezed
|
||||
class AIPromptInputState with _$AIPromptInputState {
|
||||
const factory AIPromptInputState({
|
||||
required AIModelState modelState,
|
||||
required AiType aiType,
|
||||
required bool supportChatWithFile,
|
||||
required bool showPredefinedFormats,
|
||||
required PredefinedFormat? predefinedFormat,
|
||||
required List<ChatFile> attachedFiles,
|
||||
required List<ViewPB> mentionedPages,
|
||||
required bool editable,
|
||||
required String hintText,
|
||||
}) = _AIPromptInputState;
|
||||
|
||||
factory AIPromptInputState.initial(PredefinedFormat? format) =>
|
||||
AIPromptInputState(
|
||||
modelState: AIModelState(
|
||||
type: AiType.cloud,
|
||||
isEditable: true,
|
||||
hintText: '',
|
||||
localAIEnabled: false,
|
||||
tooltip: null,
|
||||
),
|
||||
aiType: AiType.cloud,
|
||||
supportChatWithFile: false,
|
||||
showPredefinedFormats: format != null,
|
||||
predefinedFormat: format,
|
||||
attachedFiles: [],
|
||||
mentionedPages: [],
|
||||
editable: true,
|
||||
hintText: '',
|
||||
);
|
||||
}
|
||||
@@ -1,200 +0,0 @@
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
import 'ai_entities.dart';
|
||||
import 'appflowy_ai_service.dart';
|
||||
|
||||
part 'ai_prompt_selector_cubit.freezed.dart';
|
||||
|
||||
class AiPromptSelectorCubit extends Cubit<AiPromptSelectorState> {
|
||||
AiPromptSelectorCubit({
|
||||
AppFlowyAIService? aiService,
|
||||
}) : _aiService = aiService ?? AppFlowyAIService(),
|
||||
super(AiPromptSelectorState.loading()) {
|
||||
filterTextController.addListener(_filterTextChanged);
|
||||
_loadPrompts();
|
||||
}
|
||||
|
||||
final AppFlowyAIService _aiService;
|
||||
final filterTextController = TextEditingController();
|
||||
final List<AiPrompt> availablePrompts = [];
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
filterTextController.dispose();
|
||||
await super.close();
|
||||
}
|
||||
|
||||
void _loadPrompts() async {
|
||||
availablePrompts.addAll(await _aiService.getBuiltInPrompts());
|
||||
final featuredPrompts =
|
||||
availablePrompts.where((prompt) => prompt.isFeatured);
|
||||
final visiblePrompts = _getFilteredPrompts(featuredPrompts);
|
||||
emit(
|
||||
AiPromptSelectorState.ready(
|
||||
visiblePrompts: visiblePrompts,
|
||||
showCustomPrompts: false,
|
||||
isFeaturedCategorySelected: true,
|
||||
selectedPromptId: visiblePrompts.firstOrNull?.id,
|
||||
selectedCategory: null,
|
||||
favoritePrompts: [],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void selectFeaturedCategory() {
|
||||
state.maybeMap(
|
||||
ready: (readyState) {
|
||||
final prompts = availablePrompts.where((prompt) => prompt.isFeatured);
|
||||
final visiblePrompts = _getFilteredPrompts(prompts);
|
||||
emit(
|
||||
readyState.copyWith(
|
||||
visiblePrompts: visiblePrompts,
|
||||
isFeaturedCategorySelected: true,
|
||||
selectedPromptId: visiblePrompts.firstOrNull?.id,
|
||||
),
|
||||
);
|
||||
},
|
||||
orElse: () {},
|
||||
);
|
||||
}
|
||||
|
||||
void selectCategory(AiPromptCategory? category) {
|
||||
state.maybeMap(
|
||||
ready: (readyState) {
|
||||
final unfilteredPrompts = category == null
|
||||
? availablePrompts
|
||||
: availablePrompts.where((prompt) => prompt.category == category);
|
||||
final prompts = _getFilteredPrompts(unfilteredPrompts);
|
||||
|
||||
// Check if the selected prompt is still visible
|
||||
String? selectedPromptId = prompts.firstOrNull?.id;
|
||||
if (readyState.selectedPromptId != null &&
|
||||
prompts.any((prompt) => prompt.id == readyState.selectedPromptId)) {
|
||||
selectedPromptId = readyState.selectedPromptId;
|
||||
}
|
||||
|
||||
emit(
|
||||
readyState.copyWith(
|
||||
visiblePrompts: prompts,
|
||||
isFeaturedCategorySelected: false,
|
||||
selectedCategory: category,
|
||||
selectedPromptId: selectedPromptId,
|
||||
),
|
||||
);
|
||||
},
|
||||
orElse: () {},
|
||||
);
|
||||
}
|
||||
|
||||
void selectPrompt(String promptId) {
|
||||
state.maybeMap(
|
||||
ready: (readyState) {
|
||||
final selectedPrompt = readyState.visiblePrompts
|
||||
.firstWhereOrNull((prompt) => prompt.id == promptId);
|
||||
if (selectedPrompt != null) {
|
||||
emit(
|
||||
readyState.copyWith(selectedPromptId: selectedPrompt.id),
|
||||
);
|
||||
}
|
||||
},
|
||||
orElse: () {},
|
||||
);
|
||||
}
|
||||
|
||||
void toggleFavorite(String promptId) {
|
||||
state.maybeMap(
|
||||
ready: (readyState) {
|
||||
final favoritePrompts = [...readyState.favoritePrompts];
|
||||
if (favoritePrompts.contains(promptId)) {
|
||||
favoritePrompts.remove(promptId);
|
||||
} else {
|
||||
favoritePrompts.add(promptId);
|
||||
}
|
||||
emit(
|
||||
readyState.copyWith(favoritePrompts: favoritePrompts),
|
||||
);
|
||||
|
||||
// TODO(RS): Save the updated favorite prompts to local storage or database
|
||||
},
|
||||
orElse: () {},
|
||||
);
|
||||
}
|
||||
|
||||
void reset() {
|
||||
filterTextController.clear();
|
||||
state.maybeMap(
|
||||
ready: (readyState) {
|
||||
emit(
|
||||
readyState.copyWith(
|
||||
visiblePrompts: availablePrompts,
|
||||
selectedPromptId: null,
|
||||
selectedCategory: null,
|
||||
),
|
||||
);
|
||||
},
|
||||
orElse: () {},
|
||||
);
|
||||
}
|
||||
|
||||
void _filterTextChanged() {
|
||||
state.maybeMap(
|
||||
ready: (readyState) {
|
||||
final unfilteredPrompts = readyState.selectedCategory == null
|
||||
? readyState.isFeaturedCategorySelected
|
||||
? availablePrompts.where((prompt) => prompt.isFeatured)
|
||||
: availablePrompts
|
||||
: availablePrompts.where(
|
||||
(prompt) => prompt.category == readyState.selectedCategory,
|
||||
);
|
||||
final filteredPrompts = _getFilteredPrompts(unfilteredPrompts);
|
||||
|
||||
emit(
|
||||
readyState.copyWith(visiblePrompts: filteredPrompts),
|
||||
);
|
||||
},
|
||||
orElse: () {},
|
||||
);
|
||||
}
|
||||
|
||||
List<AiPrompt> _getFilteredPrompts(Iterable<AiPrompt> prompts) {
|
||||
final filterText = filterTextController.value.text.trim().toLowerCase();
|
||||
|
||||
return prompts.where((prompt) {
|
||||
final content = "${prompt.name} ${prompt.name}".toLowerCase();
|
||||
return content.contains(filterText);
|
||||
}).toList();
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
class AiPromptSelectorState with _$AiPromptSelectorState {
|
||||
const AiPromptSelectorState._();
|
||||
|
||||
const factory AiPromptSelectorState.loading() = _AiPromptSelectorLoadingState;
|
||||
|
||||
const factory AiPromptSelectorState.ready({
|
||||
required List<AiPrompt> visiblePrompts,
|
||||
required List<String> favoritePrompts,
|
||||
required bool showCustomPrompts,
|
||||
required bool isFeaturedCategorySelected,
|
||||
required AiPromptCategory? selectedCategory,
|
||||
required String? selectedPromptId,
|
||||
}) = _AiPromptSelectorReadyState;
|
||||
|
||||
bool get isLoading => this is _AiPromptSelectorLoadingState;
|
||||
bool get isReady => this is _AiPromptSelectorReadyState;
|
||||
|
||||
AiPrompt? get selectedPrompt => maybeMap(
|
||||
ready: (state) => state.visiblePrompts
|
||||
.firstWhereOrNull((prompt) => prompt.id == state.selectedPromptId),
|
||||
orElse: () => null,
|
||||
);
|
||||
|
||||
AiPromptCategory? get selectedCategory => maybeMap(
|
||||
ready: (state) => state.selectedCategory,
|
||||
orElse: () => null,
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:ffi';
|
||||
import 'dart:isolate';
|
||||
|
||||
@@ -12,7 +11,6 @@ import 'package:appflowy_backend/protobuf/flowy-ai/entities.pb.dart';
|
||||
import 'package:appflowy_result/appflowy_result.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:fixnum/fixnum.dart' as fixnum;
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'ai_entities.dart';
|
||||
import 'error.dart';
|
||||
@@ -23,7 +21,7 @@ enum LocalAIStreamingState {
|
||||
}
|
||||
|
||||
abstract class AIRepository {
|
||||
Future<(String, CompletionStream)?> streamCompletion({
|
||||
Future<void> streamCompletion({
|
||||
String? objectId,
|
||||
required String text,
|
||||
PredefinedFormat? format,
|
||||
@@ -38,10 +36,6 @@ abstract class AIRepository {
|
||||
required void Function(LocalAIStreamingState state)
|
||||
onLocalAIStreamingStateChange,
|
||||
});
|
||||
|
||||
Future<List<AiPrompt>> getBuiltInPrompts();
|
||||
|
||||
void updateFavoritePrompts(List<String> promptIds);
|
||||
}
|
||||
|
||||
class AppFlowyAIService implements AIRepository {
|
||||
@@ -93,32 +87,6 @@ class AppFlowyAIService implements AIRepository {
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<AiPrompt>> getBuiltInPrompts() async {
|
||||
final prompts = <AiPrompt>[];
|
||||
|
||||
try {
|
||||
final jsonString =
|
||||
await rootBundle.loadString('assets/built_in_prompts.json');
|
||||
// final data = await rootBundle.load('assets/built_in_prompts.json');
|
||||
// final jsonString = utf8.decode(data.buffer.asUint8List());
|
||||
final jsonData = json.decode(jsonString) as Map<String, dynamic>;
|
||||
final promptJson = jsonData['prompts'] as List<dynamic>;
|
||||
prompts.addAll(
|
||||
promptJson
|
||||
.map((e) => AiPrompt.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
);
|
||||
} catch (e) {
|
||||
Log.error(e);
|
||||
}
|
||||
|
||||
return prompts;
|
||||
}
|
||||
|
||||
@override
|
||||
void updateFavoritePrompts(List<String> promptIds) {}
|
||||
}
|
||||
|
||||
abstract class CompletionStream {
|
||||
|
||||
@@ -83,7 +83,7 @@ class SelectModelState with _$SelectModelState {
|
||||
}) = _SelectModelState;
|
||||
|
||||
factory SelectModelState.initial(AIModelStateNotifier notifier) {
|
||||
final (models, selectedModel) = notifier.getModelSelection();
|
||||
final (models, selectedModel) = notifier.getAvailableModels();
|
||||
return SelectModelState(
|
||||
models: models,
|
||||
selectedModel: selectedModel,
|
||||
|
||||
-183
@@ -1,183 +0,0 @@
|
||||
import 'package:appflowy/ai/ai.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy_ui/appflowy_ui.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class AiPromptCategoryList extends StatefulWidget {
|
||||
const AiPromptCategoryList({
|
||||
super.key,
|
||||
required this.padding,
|
||||
});
|
||||
|
||||
final EdgeInsetsGeometry padding;
|
||||
|
||||
@override
|
||||
State<AiPromptCategoryList> createState() => _AiPromptCategoryListState();
|
||||
}
|
||||
|
||||
class _AiPromptCategoryListState extends State<AiPromptCategoryList> {
|
||||
bool isSearching = false;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = AppFlowyTheme.of(context);
|
||||
|
||||
return Padding(
|
||||
padding: EdgeInsets.all(
|
||||
theme.spacing.l,
|
||||
),
|
||||
child: TextFieldTapRegion(
|
||||
groupId: "ai_prompt_category_list",
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
spacing: theme.spacing.s,
|
||||
children: [
|
||||
_buildFeaturedCategory(context),
|
||||
const AFDivider(),
|
||||
Expanded(
|
||||
child: ListView(
|
||||
padding: EdgeInsets.zero,
|
||||
children: [
|
||||
_buildCategoryItem(context, null),
|
||||
_buildCategoryItem(context, AiPromptCategory.development),
|
||||
_buildCategoryItem(context, AiPromptCategory.writing),
|
||||
_buildCategoryItem(context, AiPromptCategory.business),
|
||||
_buildCategoryItem(context, AiPromptCategory.marketing),
|
||||
_buildCategoryItem(
|
||||
context,
|
||||
AiPromptCategory.healthAndFitness,
|
||||
),
|
||||
_buildCategoryItem(context, AiPromptCategory.travel),
|
||||
_buildCategoryItem(context, AiPromptCategory.contentSeo),
|
||||
_buildCategoryItem(context, AiPromptCategory.emailMarketing),
|
||||
_buildCategoryItem(context, AiPromptCategory.paidAds),
|
||||
_buildCategoryItem(context, AiPromptCategory.prCommunication),
|
||||
_buildCategoryItem(context, AiPromptCategory.recruiting),
|
||||
_buildCategoryItem(context, AiPromptCategory.sales),
|
||||
_buildCategoryItem(context, AiPromptCategory.socialMedia),
|
||||
_buildCategoryItem(context, AiPromptCategory.strategy),
|
||||
_buildCategoryItem(context, AiPromptCategory.caseStudies),
|
||||
_buildCategoryItem(context, AiPromptCategory.salesCopy),
|
||||
_buildCategoryItem(context, AiPromptCategory.education),
|
||||
_buildCategoryItem(context, AiPromptCategory.work),
|
||||
_buildCategoryItem(
|
||||
context,
|
||||
AiPromptCategory.podcastProduction,
|
||||
),
|
||||
_buildCategoryItem(context, AiPromptCategory.copyWriting),
|
||||
_buildCategoryItem(context, AiPromptCategory.customerSuccess),
|
||||
_buildCategoryItem(context, AiPromptCategory.other),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCategoryItem(
|
||||
BuildContext context,
|
||||
AiPromptCategory? category,
|
||||
) {
|
||||
return AiPromptCategoryItem(
|
||||
category: category,
|
||||
onSelect: () {
|
||||
context.read<AiPromptSelectorCubit>().selectCategory(category);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFeaturedCategory(BuildContext context) {
|
||||
final theme = AppFlowyTheme.of(context);
|
||||
final isSelected = context.watch<AiPromptSelectorCubit>().state.maybeMap(
|
||||
ready: (state) => state.isFeaturedCategorySelected,
|
||||
orElse: () => false,
|
||||
);
|
||||
|
||||
return AFBaseButton(
|
||||
onTap: () {
|
||||
if (!isSelected) {
|
||||
context.read<AiPromptSelectorCubit>().selectFeaturedCategory();
|
||||
}
|
||||
},
|
||||
builder: (context, isHovering, disabled) {
|
||||
return Text(
|
||||
LocaleKeys.ai_customPrompt_featured.tr(),
|
||||
style: AppFlowyTheme.of(context).textStyle.body.standard(
|
||||
color: theme.textColorScheme.primary,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
);
|
||||
},
|
||||
borderRadius: theme.borderRadius.m,
|
||||
padding: EdgeInsets.symmetric(
|
||||
vertical: theme.spacing.s,
|
||||
horizontal: theme.spacing.m,
|
||||
),
|
||||
borderColor: (context, isHovering, disabled, isFocused) =>
|
||||
Colors.transparent,
|
||||
backgroundColor: (context, isHovering, disabled) {
|
||||
if (isSelected) {
|
||||
return theme.fillColorScheme.themeSelect;
|
||||
}
|
||||
if (isHovering) {
|
||||
return theme.fillColorScheme.primaryAlpha5;
|
||||
}
|
||||
return Colors.transparent;
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AiPromptCategoryItem extends StatelessWidget {
|
||||
const AiPromptCategoryItem({
|
||||
super.key,
|
||||
required this.category,
|
||||
required this.onSelect,
|
||||
});
|
||||
|
||||
final AiPromptCategory? category;
|
||||
final VoidCallback onSelect;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = AppFlowyTheme.of(context);
|
||||
final isSelected = context.watch<AiPromptSelectorCubit>().state.maybeMap(
|
||||
ready: (state) =>
|
||||
!state.isFeaturedCategorySelected &&
|
||||
state.selectedCategory == category,
|
||||
orElse: () => false,
|
||||
);
|
||||
|
||||
return AFBaseButton(
|
||||
onTap: onSelect,
|
||||
builder: (context, isHovering, disabled) {
|
||||
return Text(
|
||||
category?.i18n ?? LocaleKeys.ai_customPrompt_all.tr(),
|
||||
style: AppFlowyTheme.of(context).textStyle.body.standard(
|
||||
color: theme.textColorScheme.primary,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
);
|
||||
},
|
||||
borderRadius: theme.borderRadius.m,
|
||||
padding: EdgeInsets.symmetric(
|
||||
vertical: theme.spacing.s,
|
||||
horizontal: theme.spacing.m,
|
||||
),
|
||||
borderColor: (context, isHovering, disabled, isFocused) =>
|
||||
Colors.transparent,
|
||||
backgroundColor: (context, isHovering, disabled) {
|
||||
if (isSelected) {
|
||||
return theme.fillColorScheme.themeSelect;
|
||||
}
|
||||
if (isHovering) {
|
||||
return theme.fillColorScheme.primaryAlpha5;
|
||||
}
|
||||
return Colors.transparent;
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,129 +0,0 @@
|
||||
import 'package:appflowy/ai/ai.dart';
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy_ui/appflowy_ui.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import 'ai_prompt_category_list.dart';
|
||||
import 'ai_prompt_preview.dart';
|
||||
import 'ai_prompt_visible_list.dart';
|
||||
|
||||
Future<AiPrompt?> showAiPromptModal(
|
||||
BuildContext context, {
|
||||
required AiPromptSelectorCubit aiPromptSelectorCubit,
|
||||
}) async {
|
||||
return showDialog<AiPrompt?>(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return BlocProvider.value(
|
||||
value: aiPromptSelectorCubit,
|
||||
child: const AiPromptModal(),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
class AiPromptModal extends StatelessWidget {
|
||||
const AiPromptModal({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = AppFlowyTheme.of(context);
|
||||
|
||||
return AFModal(
|
||||
constraints: const BoxConstraints(
|
||||
maxWidth: 1200,
|
||||
maxHeight: 800,
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
AFModalHeader(
|
||||
leading: Text(
|
||||
LocaleKeys.ai_customPrompt_browsePrompts.tr(),
|
||||
style: theme.textStyle.heading4.prominent(
|
||||
color: theme.textColorScheme.primary,
|
||||
),
|
||||
),
|
||||
trailing: [
|
||||
AFGhostButton.normal(
|
||||
onTap: () => Navigator.of(context).pop(),
|
||||
padding: EdgeInsets.all(theme.spacing.s),
|
||||
builder: (context, isHovering, disabled) {
|
||||
return Center(
|
||||
child: FlowySvg(
|
||||
FlowySvgs.toast_close_s,
|
||||
size: Size.square(20),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
Expanded(
|
||||
child: AFModalBody(
|
||||
child: BlocBuilder<AiPromptSelectorCubit, AiPromptSelectorState>(
|
||||
builder: (context, state) {
|
||||
final theme = AppFlowyTheme.of(context);
|
||||
|
||||
return state.map(
|
||||
loading: (_) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
},
|
||||
ready: (_) {
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Expanded(
|
||||
child: AiPromptCategoryList(
|
||||
padding: EdgeInsetsDirectional.only(
|
||||
top: theme.spacing.l,
|
||||
end: theme.spacing.l,
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: AiPromptVisibleList(),
|
||||
),
|
||||
Expanded(
|
||||
flex: 3,
|
||||
child: BlocBuilder<AiPromptSelectorCubit,
|
||||
AiPromptSelectorState>(
|
||||
builder: (context, state) {
|
||||
final selectedPrompt = state.maybeMap(
|
||||
ready: (state) {
|
||||
return state.visiblePrompts
|
||||
.firstWhereOrNull(
|
||||
(prompt) =>
|
||||
prompt.id == state.selectedPromptId,
|
||||
);
|
||||
},
|
||||
orElse: () => null,
|
||||
);
|
||||
if (selectedPrompt == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
return AiPromptPreview(
|
||||
prompt: selectedPrompt,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,189 +0,0 @@
|
||||
import 'package:appflowy/ai/ai.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/ai_chat/presentation/message/ai_markdown_text.dart';
|
||||
import 'package:appflowy_ui/appflowy_ui.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/widget/spacing.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class AiPromptPreview extends StatelessWidget {
|
||||
const AiPromptPreview({
|
||||
super.key,
|
||||
required this.prompt,
|
||||
});
|
||||
|
||||
final AiPrompt prompt;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = AppFlowyTheme.of(context);
|
||||
|
||||
return SelectionArea(
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: EdgeInsets.only(
|
||||
top: theme.spacing.l,
|
||||
left: theme.spacing.l,
|
||||
right: theme.spacing.l,
|
||||
),
|
||||
child: SelectionContainer.disabled(
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
prompt.name,
|
||||
style: theme.textStyle.headline.standard(
|
||||
color: theme.textColorScheme.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
HSpace(theme.spacing.s),
|
||||
AFFilledTextButton.primary(
|
||||
text: LocaleKeys.ai_customPrompt_usePrompt.tr(),
|
||||
onTap: () {
|
||||
Navigator.of(context).pop(prompt);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
VSpace(theme.spacing.xs),
|
||||
Expanded(
|
||||
child: ListView(
|
||||
padding: EdgeInsets.all(
|
||||
theme.spacing.l,
|
||||
),
|
||||
children: [
|
||||
SelectionContainer.disabled(
|
||||
child: Text(
|
||||
LocaleKeys.ai_customPrompt_prompt.tr(),
|
||||
style: theme.textStyle.heading4.standard(
|
||||
color: theme.textColorScheme.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
VSpace(theme.spacing.xs),
|
||||
_PromptContent(
|
||||
prompt: prompt,
|
||||
),
|
||||
VSpace(theme.spacing.xl),
|
||||
if (prompt.example.isNotEmpty) ...[
|
||||
SelectionContainer.disabled(
|
||||
child: Text(
|
||||
LocaleKeys.ai_customPrompt_example.tr(),
|
||||
style: theme.textStyle.heading4.standard(
|
||||
color: theme.textColorScheme.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
VSpace(theme.spacing.xs),
|
||||
_PromptExample(
|
||||
prompt: prompt,
|
||||
),
|
||||
VSpace(theme.spacing.xl),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PromptContent extends StatelessWidget {
|
||||
const _PromptContent({
|
||||
required this.prompt,
|
||||
});
|
||||
|
||||
final AiPrompt prompt;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = AppFlowyTheme.of(context);
|
||||
final textSpans = _buildTextSpans(context, prompt.content);
|
||||
|
||||
return Container(
|
||||
padding: EdgeInsets.all(theme.spacing.l),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.fillColorScheme.quaternary,
|
||||
borderRadius: BorderRadius.circular(theme.borderRadius.m),
|
||||
),
|
||||
child: Text.rich(
|
||||
TextSpan(
|
||||
style: theme.textStyle.body.standard(
|
||||
color: theme.textColorScheme.primary,
|
||||
),
|
||||
children: textSpans,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<TextSpan> _buildTextSpans(BuildContext context, String text) {
|
||||
final theme = AppFlowyTheme.of(context);
|
||||
final spans = <TextSpan>[];
|
||||
|
||||
final parts = _splitPromptText(text);
|
||||
for (final part in parts) {
|
||||
if (part.startsWith('[') && part.endsWith(']')) {
|
||||
spans.add(
|
||||
TextSpan(
|
||||
text: part,
|
||||
style: TextStyle(color: theme.textColorScheme.purple),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
spans.add(TextSpan(text: part));
|
||||
}
|
||||
}
|
||||
|
||||
return spans;
|
||||
}
|
||||
|
||||
List<String> _splitPromptText(String text) {
|
||||
final regex = RegExp(r'(\[[^\[\]]*?\])');
|
||||
|
||||
final result = <String>[];
|
||||
|
||||
text.splitMapJoin(
|
||||
regex,
|
||||
onMatch: (match) {
|
||||
result.add(match.group(0)!);
|
||||
return '';
|
||||
},
|
||||
onNonMatch: (nonMatch) {
|
||||
result.add(nonMatch);
|
||||
return '';
|
||||
},
|
||||
);
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
class _PromptExample extends StatelessWidget {
|
||||
const _PromptExample({
|
||||
required this.prompt,
|
||||
});
|
||||
|
||||
final AiPrompt prompt;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = AppFlowyTheme.of(context);
|
||||
|
||||
return Container(
|
||||
padding: EdgeInsets.all(theme.spacing.l),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.fillColorScheme.quaternary,
|
||||
borderRadius: BorderRadius.circular(theme.borderRadius.m),
|
||||
),
|
||||
child: AIMarkdownText(
|
||||
markdown: prompt.example,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,341 +0,0 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:appflowy/ai/ai.dart';
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/util/theme_extension.dart';
|
||||
import 'package:appflowy_ui/appflowy_ui.dart';
|
||||
import 'package:diffutil_dart/diffutil.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/widget/spacing.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
const Duration _listItemAnimationDuration = Duration(milliseconds: 150);
|
||||
|
||||
class AiPromptVisibleList extends StatefulWidget {
|
||||
const AiPromptVisibleList({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
State<AiPromptVisibleList> createState() => _AiPromptVisibleListState();
|
||||
}
|
||||
|
||||
class _AiPromptVisibleListState extends State<AiPromptVisibleList> {
|
||||
final listKey = GlobalKey<AnimatedListState>();
|
||||
final scrollController = ScrollController();
|
||||
final List<AiPrompt> oldList = [];
|
||||
|
||||
late AiPromptSelectorCubit cubit;
|
||||
late bool filterIsEmpty;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
cubit = context.read<AiPromptSelectorCubit>();
|
||||
final textController = cubit.filterTextController;
|
||||
final prompts = cubit.state.maybeMap(
|
||||
ready: (value) => value.visiblePrompts,
|
||||
orElse: () => <AiPrompt>[],
|
||||
);
|
||||
filterIsEmpty = textController.text.isEmpty;
|
||||
textController.addListener(handleFilterTextChanged);
|
||||
oldList.addAll(prompts);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
cubit.filterTextController.removeListener(handleFilterTextChanged);
|
||||
scrollController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final spacing = AppFlowyTheme.of(context).spacing;
|
||||
return BlocListener<AiPromptSelectorCubit, AiPromptSelectorState>(
|
||||
listener: (context, state) {
|
||||
final list = state.maybeMap(
|
||||
ready: (state) => state.visiblePrompts,
|
||||
orElse: () => <AiPrompt>[],
|
||||
);
|
||||
handleVisiblePromptListChanged(list);
|
||||
},
|
||||
child: Column(
|
||||
children: [
|
||||
VSpace(spacing.l),
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: spacing.l),
|
||||
child: buildSearchField(context),
|
||||
),
|
||||
VSpace(spacing.s),
|
||||
Expanded(
|
||||
child: AnimatedList(
|
||||
controller: scrollController,
|
||||
padding: EdgeInsets.all(spacing.l),
|
||||
key: listKey,
|
||||
initialItemCount: oldList.length,
|
||||
itemBuilder: (context, index, animation) {
|
||||
final cubit = context.read<AiPromptSelectorCubit>();
|
||||
|
||||
return cubit.state.maybeMap(
|
||||
ready: (state) {
|
||||
final prompt = state.visiblePrompts[index];
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(
|
||||
top: index == 0 ? 0 : spacing.s,
|
||||
bottom: index == state.visiblePrompts.length - 1
|
||||
? 0
|
||||
: spacing.s,
|
||||
),
|
||||
child: _AiPromptListItem(
|
||||
animation: animation,
|
||||
prompt: prompt,
|
||||
isSelected: state.selectedPromptId == prompt.id,
|
||||
),
|
||||
);
|
||||
},
|
||||
orElse: () => const SizedBox.shrink(),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildSearchField(BuildContext context) {
|
||||
final theme = AppFlowyTheme.of(context);
|
||||
final iconSize = 20.0;
|
||||
|
||||
return AFTextField(
|
||||
groupId: "ai_prompt_category_list",
|
||||
hintText: "Search",
|
||||
controller: context.read<AiPromptSelectorCubit>().filterTextController,
|
||||
autoFocus: true,
|
||||
suffixIconConstraints: BoxConstraints.tightFor(
|
||||
width: iconSize + theme.spacing.m,
|
||||
height: iconSize,
|
||||
),
|
||||
suffixIconBuilder: filterIsEmpty
|
||||
? null
|
||||
: (context, isObscured) => TextFieldTapRegion(
|
||||
groupId: "ai_prompt_category_list",
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(right: theme.spacing.m),
|
||||
child: GestureDetector(
|
||||
onTap: () => context
|
||||
.read<AiPromptSelectorCubit>()
|
||||
.filterTextController
|
||||
.clear(),
|
||||
child: FlowySvg(
|
||||
FlowySvgs.toast_close_s,
|
||||
color: theme.textColorScheme.secondary,
|
||||
size: const Size.square(20),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void handleVisiblePromptListChanged(
|
||||
List<AiPrompt> newList,
|
||||
) {
|
||||
final updates = calculateListDiff(oldList, newList).getUpdatesWithData();
|
||||
|
||||
for (final update in updates) {
|
||||
update.when(
|
||||
insert: (pos, data) {
|
||||
listKey.currentState?.insertItem(
|
||||
pos,
|
||||
duration: _listItemAnimationDuration,
|
||||
);
|
||||
},
|
||||
remove: (pos, data) {
|
||||
listKey.currentState?.removeItem(
|
||||
pos,
|
||||
(context, animation) {
|
||||
final isSelected =
|
||||
context.read<AiPromptSelectorCubit>().state.maybeMap(
|
||||
ready: (state) => state.selectedPromptId == data.id,
|
||||
orElse: () => false,
|
||||
);
|
||||
return _AiPromptListItem(
|
||||
animation: animation,
|
||||
prompt: data,
|
||||
isSelected: isSelected,
|
||||
);
|
||||
},
|
||||
duration: _listItemAnimationDuration,
|
||||
);
|
||||
},
|
||||
change: (pos, oldData, newData) {},
|
||||
move: (from, to, data) {},
|
||||
);
|
||||
}
|
||||
oldList
|
||||
..clear()
|
||||
..addAll(newList);
|
||||
}
|
||||
|
||||
void handleFilterTextChanged() {
|
||||
setState(() {
|
||||
filterIsEmpty = cubit.filterTextController.text.isEmpty;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class _AiPromptListItem extends StatefulWidget {
|
||||
const _AiPromptListItem({
|
||||
required this.animation,
|
||||
required this.prompt,
|
||||
required this.isSelected,
|
||||
});
|
||||
|
||||
final Animation<double> animation;
|
||||
final AiPrompt prompt;
|
||||
final bool isSelected;
|
||||
|
||||
@override
|
||||
State<_AiPromptListItem> createState() => _AiPromptListItemState();
|
||||
}
|
||||
|
||||
class _AiPromptListItemState extends State<_AiPromptListItem> {
|
||||
bool isHovering = false;
|
||||
Timer? timer;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
timer?.cancel();
|
||||
timer = null;
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = AppFlowyTheme.of(context);
|
||||
final cubit = context.read<AiPromptSelectorCubit>();
|
||||
|
||||
final curvedAnimation = CurvedAnimation(
|
||||
parent: widget.animation,
|
||||
curve: Curves.easeIn,
|
||||
);
|
||||
|
||||
final surfacePrimaryHover =
|
||||
Theme.of(context).isLightMode ? Color(0xFFF8FAFF) : Color(0xFF3C3F4E);
|
||||
|
||||
return FadeTransition(
|
||||
opacity: curvedAnimation,
|
||||
child: SizeTransition(
|
||||
sizeFactor: curvedAnimation,
|
||||
child: MouseRegion(
|
||||
onEnter: (_) {
|
||||
setState(() {
|
||||
isHovering = true;
|
||||
timer = Timer(const Duration(milliseconds: 300), () {
|
||||
if (mounted) {
|
||||
cubit.selectPrompt(widget.prompt.id);
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
onExit: (_) {
|
||||
setState(() {
|
||||
isHovering = false;
|
||||
timer?.cancel();
|
||||
});
|
||||
},
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
cubit.selectPrompt(widget.prompt.id);
|
||||
},
|
||||
child: Stack(
|
||||
children: [
|
||||
Container(
|
||||
padding: EdgeInsets.all(theme.spacing.m),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(theme.borderRadius.m),
|
||||
border: Border.all(
|
||||
color: widget.isSelected
|
||||
? isHovering
|
||||
? theme.borderColorScheme.themeThickHover
|
||||
: theme.borderColorScheme.themeThick
|
||||
: isHovering
|
||||
? theme.borderColorScheme.greyTertiaryHover
|
||||
: theme.borderColorScheme.greyTertiary,
|
||||
),
|
||||
color: theme.surfaceColorScheme.primary,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
widget.prompt.name,
|
||||
maxLines: 1,
|
||||
style: theme.textStyle.body.standard(
|
||||
color: theme.textColorScheme.primary,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
softWrap: true,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Text(
|
||||
widget.prompt.content,
|
||||
maxLines: 2,
|
||||
style: theme.textStyle.caption.standard(
|
||||
color: theme.textColorScheme.secondary,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
softWrap: true,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (isHovering)
|
||||
Positioned(
|
||||
top: theme.spacing.s,
|
||||
right: theme.spacing.s,
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(boxShadow: theme.shadow.small),
|
||||
child: AFBaseButton(
|
||||
onTap: () {
|
||||
Navigator.of(context).pop(widget.prompt);
|
||||
},
|
||||
builder: (context, isHovering, disabled) {
|
||||
return Text(
|
||||
LocaleKeys.ai_customPrompt_usePrompt.tr(),
|
||||
style: theme.textStyle.body.standard(
|
||||
color: theme.textColorScheme.primary,
|
||||
),
|
||||
);
|
||||
},
|
||||
backgroundColor: (context, isHovering, disabled) {
|
||||
if (isHovering) {
|
||||
return surfacePrimaryHover;
|
||||
}
|
||||
return theme.surfaceColorScheme.primary;
|
||||
},
|
||||
padding: EdgeInsets.symmetric(
|
||||
vertical: theme.spacing.s,
|
||||
horizontal: theme.spacing.m,
|
||||
),
|
||||
borderRadius: theme.borderRadius.m,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
import 'package:appflowy/ai/ai.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/hover.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import '../ai_prompt_modal/ai_prompt_modal.dart';
|
||||
|
||||
class BrowsePromptsButton extends StatelessWidget {
|
||||
const BrowsePromptsButton({
|
||||
super.key,
|
||||
required this.onSelectPrompt,
|
||||
});
|
||||
|
||||
final void Function(AiPrompt) onSelectPrompt;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FlowyTooltip(
|
||||
message: LocaleKeys.ai_customPrompt_browsePrompts.tr(),
|
||||
child: BlocProvider(
|
||||
create: (context) => AiPromptSelectorCubit(),
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
return GestureDetector(
|
||||
onTap: () async {
|
||||
final prompt = await showAiPromptModal(
|
||||
context,
|
||||
aiPromptSelectorCubit: context.read<AiPromptSelectorCubit>(),
|
||||
);
|
||||
if (context.mounted) {
|
||||
context.read<AiPromptSelectorCubit>().reset();
|
||||
}
|
||||
if (prompt != null && context.mounted) {
|
||||
onSelectPrompt(prompt);
|
||||
}
|
||||
},
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: SizedBox(
|
||||
height: DesktopAIPromptSizes.actionBarButtonSize,
|
||||
child: FlowyHover(
|
||||
style: const HoverStyle(
|
||||
borderRadius: BorderRadius.all(Radius.circular(8)),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsetsDirectional.all(4.0),
|
||||
child: Center(
|
||||
child: FlowyText(
|
||||
LocaleKeys.ai_customPrompt_browsePrompts.tr(),
|
||||
fontSize: 12,
|
||||
figmaLineHeight: 16,
|
||||
color: Theme.of(context).hintColor,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+57
-79
@@ -1,19 +1,18 @@
|
||||
import 'package:appflowy/ai/ai.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/ai_chat/application/chat_input_control_cubit.dart';
|
||||
import 'package:appflowy/plugins/ai_chat/application/chat_user_cubit.dart';
|
||||
import 'package:appflowy/plugins/ai_chat/presentation/layout_define.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/util/theme_extension.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
|
||||
import 'package:appflowy_ui/appflowy_ui.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:extended_text_field/extended_text_field.dart';
|
||||
import 'package:flowy_infra/file_picker/file_picker_service.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import 'browse_prompts_button.dart';
|
||||
|
||||
class DesktopPromptInput extends StatefulWidget {
|
||||
const DesktopPromptInput({
|
||||
super.key,
|
||||
@@ -29,7 +28,7 @@ class DesktopPromptInput extends StatefulWidget {
|
||||
});
|
||||
|
||||
final bool isStreaming;
|
||||
final AiPromptInputTextEditingController textController;
|
||||
final TextEditingController textController;
|
||||
final void Function() onStopStreaming;
|
||||
final void Function(String, PredefinedFormat?, Map<String, dynamic>)
|
||||
onSubmitted;
|
||||
@@ -48,7 +47,6 @@ class _DesktopPromptInputState extends State<DesktopPromptInput> {
|
||||
final layerLink = LayerLink();
|
||||
final overlayController = OverlayPortalController();
|
||||
final inputControlCubit = ChatInputControlCubit();
|
||||
final chatUserCubit = ChatUserCubit();
|
||||
final focusNode = FocusNode();
|
||||
|
||||
late SendButtonState sendButtonState;
|
||||
@@ -57,7 +55,6 @@ class _DesktopPromptInputState extends State<DesktopPromptInput> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
chatUserCubit.fetchUserProfile();
|
||||
|
||||
widget.textController.addListener(handleTextControllerChanged);
|
||||
focusNode
|
||||
@@ -91,17 +88,13 @@ class _DesktopPromptInputState extends State<DesktopPromptInput> {
|
||||
focusNode.dispose();
|
||||
widget.textController.removeListener(handleTextControllerChanged);
|
||||
inputControlCubit.close();
|
||||
chatUserCubit.close();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider.value(value: inputControlCubit),
|
||||
BlocProvider.value(value: chatUserCubit),
|
||||
],
|
||||
return BlocProvider.value(
|
||||
value: inputControlCubit,
|
||||
child: BlocListener<ChatInputControlCubit, ChatInputControlState>(
|
||||
listener: (context, state) {
|
||||
state.maybeWhen(
|
||||
@@ -161,8 +154,7 @@ class _DesktopPromptInputState extends State<DesktopPromptInput> {
|
||||
start: 8.0,
|
||||
),
|
||||
child: ChangeFormatBar(
|
||||
showImageFormats:
|
||||
state.modelState.type == AiType.cloud,
|
||||
showImageFormats: state.aiType.isCloud,
|
||||
predefinedFormat: state.predefinedFormat,
|
||||
spacing: 4.0,
|
||||
onSelectPredefinedFormat: (format) =>
|
||||
@@ -194,7 +186,6 @@ class _DesktopPromptInputState extends State<DesktopPromptInput> {
|
||||
widget.selectedSourcesNotifier,
|
||||
onUpdateSelectedSources:
|
||||
widget.onUpdateSelectedSources,
|
||||
onSelectPrompt: handleOnSelectPrompt,
|
||||
extraBottomActionButton:
|
||||
widget.extraBottomActionButton,
|
||||
),
|
||||
@@ -267,12 +258,11 @@ class _DesktopPromptInputState extends State<DesktopPromptInput> {
|
||||
if (widget.isStreaming) {
|
||||
return;
|
||||
}
|
||||
String userInput = widget.textController.text.trim();
|
||||
userInput = inputControlCubit.formatIntputText(userInput);
|
||||
userInput = AiPromptInputTextEditingController.restore(userInput);
|
||||
|
||||
final trimmedText = inputControlCubit.formatIntputText(
|
||||
widget.textController.text.trim(),
|
||||
);
|
||||
widget.textController.clear();
|
||||
if (userInput.isEmpty) {
|
||||
if (trimmedText.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -284,7 +274,7 @@ class _DesktopPromptInputState extends State<DesktopPromptInput> {
|
||||
final predefinedFormat = bloc.state.predefinedFormat;
|
||||
|
||||
widget.onSubmitted(
|
||||
userInput,
|
||||
trimmedText,
|
||||
showPredefinedFormats ? predefinedFormat : null,
|
||||
metadata,
|
||||
);
|
||||
@@ -402,18 +392,20 @@ class _DesktopPromptInputState extends State<DesktopPromptInput> {
|
||||
builder: (context, state) {
|
||||
Widget textField = PromptInputTextField(
|
||||
key: textFieldKey,
|
||||
editable: state.modelState.isEditable,
|
||||
editable: state.editable,
|
||||
cubit: inputControlCubit,
|
||||
textController: widget.textController,
|
||||
textFieldFocusNode: focusNode,
|
||||
contentPadding:
|
||||
calculateContentPadding(state.showPredefinedFormats),
|
||||
hintText: state.modelState.hintText,
|
||||
hintText: state.hintText,
|
||||
);
|
||||
|
||||
if (state.modelState.tooltip != null) {
|
||||
if (!state.editable) {
|
||||
textField = FlowyTooltip(
|
||||
message: state.modelState.tooltip!,
|
||||
message: LocaleKeys
|
||||
.settings_aiPage_keys_localAINotReadyTextFieldPrompt
|
||||
.tr(),
|
||||
child: textField,
|
||||
);
|
||||
}
|
||||
@@ -502,28 +494,6 @@ class _DesktopPromptInputState extends State<DesktopPromptInput> {
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
void handleOnSelectPrompt(AiPrompt prompt) {
|
||||
final bloc = context.read<AIPromptInputBloc>();
|
||||
bloc.add(
|
||||
AIPromptInputEvent.updateMentionedViews([]),
|
||||
);
|
||||
|
||||
final content = AiPromptInputTextEditingController.replace(prompt.content);
|
||||
|
||||
widget.textController.value = TextEditingValue(
|
||||
text: content,
|
||||
selection: TextSelection.collapsed(
|
||||
offset: content.length,
|
||||
),
|
||||
);
|
||||
|
||||
if (bloc.state.showPredefinedFormats) {
|
||||
bloc.add(
|
||||
AIPromptInputEvent.toggleShowPredefinedFormat(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _SubmitOrMentionPageIntent extends Intent {
|
||||
@@ -562,9 +532,7 @@ class PromptInputTextField extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = AppFlowyTheme.of(context);
|
||||
|
||||
return TextField(
|
||||
return ExtendedTextField(
|
||||
controller: textController,
|
||||
focusNode: textFieldFocusNode,
|
||||
readOnly: !editable,
|
||||
@@ -583,14 +551,19 @@ class PromptInputTextField extends StatelessWidget {
|
||||
textCapitalization: TextCapitalization.sentences,
|
||||
minLines: 1,
|
||||
maxLines: null,
|
||||
style: theme.textStyle.body.standard(
|
||||
color: theme.textColorScheme.primary,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
specialTextSpanBuilder: PromptInputTextSpanBuilder(
|
||||
inputControlCubit: cubit,
|
||||
specialTextStyle: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
TextStyle? inputHintTextStyle(BuildContext context) {
|
||||
return AppFlowyTheme.of(context).textStyle.body.standard(
|
||||
return Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Theme.of(context).isLightMode
|
||||
? const Color(0xFFBDC2C8)
|
||||
: const Color(0xFF3C3E51),
|
||||
@@ -609,7 +582,6 @@ class _PromptBottomActions extends StatelessWidget {
|
||||
required this.onStopStreaming,
|
||||
required this.selectedSourcesNotifier,
|
||||
required this.onUpdateSelectedSources,
|
||||
required this.onSelectPrompt,
|
||||
this.extraBottomActionButton,
|
||||
});
|
||||
|
||||
@@ -622,7 +594,6 @@ class _PromptBottomActions extends StatelessWidget {
|
||||
final void Function() onStopStreaming;
|
||||
final ValueNotifier<List<String>> selectedSourcesNotifier;
|
||||
final void Function(List<String>) onUpdateSelectedSources;
|
||||
final void Function(AiPrompt) onSelectPrompt;
|
||||
final Widget? extraBottomActionButton;
|
||||
|
||||
@override
|
||||
@@ -633,20 +604,40 @@ class _PromptBottomActions extends StatelessWidget {
|
||||
child: BlocBuilder<AIPromptInputBloc, AIPromptInputState>(
|
||||
builder: (context, state) {
|
||||
return Row(
|
||||
spacing: DesktopAIChatSizes.inputActionBarButtonSpacing,
|
||||
children: [
|
||||
if (showPredefinedFormatButton) _predefinedFormatButton(),
|
||||
_selectModelButton(context),
|
||||
_buildBrowsePromptsButton(),
|
||||
|
||||
if (showPredefinedFormatButton) ...[
|
||||
_predefinedFormatButton(),
|
||||
const HSpace(
|
||||
DesktopAIChatSizes.inputActionBarButtonSpacing,
|
||||
),
|
||||
],
|
||||
SelectModelMenu(
|
||||
aiModelStateNotifier:
|
||||
context.read<AIPromptInputBloc>().aiModelStateNotifier,
|
||||
),
|
||||
const Spacer(),
|
||||
|
||||
if (context.read<ChatUserCubit>().supportSelectSource())
|
||||
if (state.aiType.isCloud) ...[
|
||||
_selectSourcesButton(),
|
||||
|
||||
if (extraBottomActionButton != null) extraBottomActionButton!,
|
||||
const HSpace(
|
||||
DesktopAIChatSizes.inputActionBarButtonSpacing,
|
||||
),
|
||||
],
|
||||
if (extraBottomActionButton != null) ...[
|
||||
extraBottomActionButton!,
|
||||
const HSpace(
|
||||
DesktopAIChatSizes.inputActionBarButtonSpacing,
|
||||
),
|
||||
],
|
||||
// _mentionButton(context),
|
||||
if (state.supportChatWithFile) _attachmentButton(context),
|
||||
// const HSpace(
|
||||
// DesktopAIPromptSizes.actionBarButtonSpacing,
|
||||
// ),
|
||||
if (state.supportChatWithFile) ...[
|
||||
_attachmentButton(context),
|
||||
const HSpace(
|
||||
DesktopAIChatSizes.inputActionBarButtonSpacing,
|
||||
),
|
||||
],
|
||||
_sendButton(),
|
||||
],
|
||||
);
|
||||
@@ -669,19 +660,6 @@ class _PromptBottomActions extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _selectModelButton(BuildContext context) {
|
||||
return SelectModelMenu(
|
||||
aiModelStateNotifier:
|
||||
context.read<AIPromptInputBloc>().aiModelStateNotifier,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBrowsePromptsButton() {
|
||||
return BrowsePromptsButton(
|
||||
onSelectPrompt: onSelectPrompt,
|
||||
);
|
||||
}
|
||||
|
||||
// Widget _mentionButton(BuildContext context) {
|
||||
// return PromptInputMentionButton(
|
||||
// iconSize: DesktopAIPromptSizes.actionBarIconSize,
|
||||
+14
-13
@@ -8,11 +8,11 @@ import 'package:flutter/material.dart';
|
||||
class PromptInputTextSpanBuilder extends SpecialTextSpanBuilder {
|
||||
PromptInputTextSpanBuilder({
|
||||
required this.inputControlCubit,
|
||||
this.mentionedPageTextStyle,
|
||||
this.specialTextStyle,
|
||||
});
|
||||
|
||||
final ChatInputControlCubit inputControlCubit;
|
||||
final TextStyle? mentionedPageTextStyle;
|
||||
final TextStyle? specialTextStyle;
|
||||
|
||||
@override
|
||||
SpecialText? createSpecialText(
|
||||
@@ -25,22 +25,23 @@ class PromptInputTextSpanBuilder extends SpecialTextSpanBuilder {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isStart(flag, MentionedPageText.flag)) {
|
||||
return MentionedPageText(
|
||||
inputControlCubit,
|
||||
mentionedPageTextStyle ?? textStyle,
|
||||
onTap,
|
||||
// scrubbing over text is kinda funky
|
||||
start: index! - (MentionedPageText.flag.length - 1),
|
||||
);
|
||||
if (!isStart(flag, AtText.flag)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return null;
|
||||
// index is at the end of the start flag, so the start index should be index - (flag.length - 1)
|
||||
return AtText(
|
||||
inputControlCubit,
|
||||
specialTextStyle ?? textStyle,
|
||||
onTap,
|
||||
// scrubbing over text is kinda funky
|
||||
start: index! - (AtText.flag.length - 1),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MentionedPageText extends SpecialText {
|
||||
MentionedPageText(
|
||||
class AtText extends SpecialText {
|
||||
AtText(
|
||||
this.inputControlCubit,
|
||||
TextStyle? textStyle,
|
||||
SpecialTextGestureTapCallback? onTap, {
|
||||
|
||||
-79
@@ -1,79 +0,0 @@
|
||||
import 'package:appflowy_ui/appflowy_ui.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
final openingBracketReplacement = String.fromCharCode(0xFFFE);
|
||||
final closingBracketReplacement = String.fromCharCode(0xFFFD);
|
||||
|
||||
class AiPromptInputTextEditingController extends TextEditingController {
|
||||
AiPromptInputTextEditingController();
|
||||
|
||||
static String replace(String text) {
|
||||
return text
|
||||
.replaceAll('[', openingBracketReplacement)
|
||||
.replaceAll(']', closingBracketReplacement);
|
||||
}
|
||||
|
||||
static String restore(String text) {
|
||||
return text
|
||||
.replaceAll(openingBracketReplacement, '[')
|
||||
.replaceAll(closingBracketReplacement, ']');
|
||||
}
|
||||
|
||||
void usePrompt(String content) {
|
||||
value = TextEditingValue(
|
||||
text: content,
|
||||
selection: TextSelection.collapsed(
|
||||
offset: content.length,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
TextSpan buildTextSpan({
|
||||
required BuildContext context,
|
||||
TextStyle? style,
|
||||
required bool withComposing,
|
||||
}) {
|
||||
return TextSpan(
|
||||
style: style,
|
||||
children: <InlineSpan>[...getTextSpans(context)],
|
||||
);
|
||||
}
|
||||
|
||||
Iterable<TextSpan> getTextSpans(BuildContext context) {
|
||||
final open = openingBracketReplacement;
|
||||
final close = closingBracketReplacement;
|
||||
final regex = RegExp('($open[^$open$close]*?$close)');
|
||||
final theme = AppFlowyTheme.of(context);
|
||||
|
||||
final result = <TextSpan>[];
|
||||
|
||||
text.splitMapJoin(
|
||||
regex,
|
||||
onMatch: (match) {
|
||||
final string = match.group(0)!;
|
||||
result.add(
|
||||
TextSpan(
|
||||
text: restore(string),
|
||||
style: theme.textStyle.body.standard().copyWith(
|
||||
color: theme.textColorScheme.purple,
|
||||
backgroundColor:
|
||||
theme.fillColorScheme.purpleThick.withAlpha(51),
|
||||
),
|
||||
),
|
||||
);
|
||||
return '';
|
||||
},
|
||||
onNonMatch: (nonMatch) {
|
||||
result.add(
|
||||
TextSpan(
|
||||
text: restore(nonMatch),
|
||||
),
|
||||
);
|
||||
return '';
|
||||
},
|
||||
);
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -31,6 +31,9 @@ class _SelectModelMenuState extends State<SelectModelMenu> {
|
||||
),
|
||||
child: BlocBuilder<SelectModelBloc, SelectModelState>(
|
||||
builder: (context, state) {
|
||||
if (state.selectedModel == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
return AppFlowyPopover(
|
||||
offset: Offset(-12.0, 0.0),
|
||||
constraints: BoxConstraints(maxWidth: 250, maxHeight: 600),
|
||||
@@ -52,12 +55,8 @@ class _SelectModelMenuState extends State<SelectModelMenu> {
|
||||
);
|
||||
},
|
||||
child: _CurrentModelButton(
|
||||
model: state.selectedModel,
|
||||
onTap: () {
|
||||
if (state.selectedModel != null) {
|
||||
popoverController.show();
|
||||
}
|
||||
},
|
||||
model: state.selectedModel!,
|
||||
onTap: () => popoverController.show(),
|
||||
),
|
||||
);
|
||||
},
|
||||
@@ -90,40 +89,38 @@ class SelectModelPopoverContent extends StatelessWidget {
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (localModels.isNotEmpty) ...[
|
||||
_ModelSectionHeader(
|
||||
title: LocaleKeys.chat_switchModel_localModel.tr(),
|
||||
),
|
||||
const VSpace(4.0),
|
||||
],
|
||||
...localModels.map(
|
||||
(model) => _ModelItem(
|
||||
model: model,
|
||||
isSelected: model == selectedModel,
|
||||
onTap: () => onSelectModel?.call(model),
|
||||
),
|
||||
),
|
||||
if (cloudModels.isNotEmpty && localModels.isNotEmpty) ...[
|
||||
const VSpace(8.0),
|
||||
_ModelSectionHeader(
|
||||
title: LocaleKeys.chat_switchModel_cloudModel.tr(),
|
||||
),
|
||||
const VSpace(4.0),
|
||||
],
|
||||
...cloudModels.map(
|
||||
(model) => _ModelItem(
|
||||
model: model,
|
||||
isSelected: model == selectedModel,
|
||||
onTap: () => onSelectModel?.call(model),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (localModels.isNotEmpty) ...[
|
||||
_ModelSectionHeader(
|
||||
title: LocaleKeys.chat_switchModel_localModel.tr(),
|
||||
),
|
||||
const VSpace(4.0),
|
||||
],
|
||||
),
|
||||
...localModels.map(
|
||||
(model) => _ModelItem(
|
||||
model: model,
|
||||
isSelected: model == selectedModel,
|
||||
onTap: () => onSelectModel?.call(model),
|
||||
),
|
||||
),
|
||||
if (cloudModels.isNotEmpty && localModels.isNotEmpty) ...[
|
||||
const VSpace(8.0),
|
||||
_ModelSectionHeader(
|
||||
title: LocaleKeys.chat_switchModel_cloudModel.tr(),
|
||||
),
|
||||
const VSpace(4.0),
|
||||
],
|
||||
...cloudModels.map(
|
||||
(model) => _ModelItem(
|
||||
model: model,
|
||||
isSelected: model == selectedModel,
|
||||
onTap: () => onSelectModel?.call(model),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -205,7 +202,7 @@ class _CurrentModelButton extends StatelessWidget {
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
final AIModelPB? model;
|
||||
final AIModelPB model;
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
@@ -224,7 +221,6 @@ class _CurrentModelButton extends StatelessWidget {
|
||||
child: Padding(
|
||||
padding: const EdgeInsetsDirectional.all(4.0),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Padding(
|
||||
// TODO: remove this after change icon to 20px
|
||||
@@ -235,11 +231,11 @@ class _CurrentModelButton extends StatelessWidget {
|
||||
size: Size.square(16),
|
||||
),
|
||||
),
|
||||
if (model != null && !model!.isDefault)
|
||||
if (!model.isDefault)
|
||||
Padding(
|
||||
padding: EdgeInsetsDirectional.only(end: 2.0),
|
||||
child: FlowyText(
|
||||
model!.i18n,
|
||||
model.i18n,
|
||||
fontSize: 12,
|
||||
figmaLineHeight: 16,
|
||||
color: Theme.of(context).hintColor,
|
||||
|
||||
+1
-1
@@ -54,7 +54,7 @@ class _PromptInputMobileSelectSourcesButtonState
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<UserWorkspaceBloc, UserWorkspaceState>(
|
||||
builder: (context, state) {
|
||||
final userProfile = context.read<UserWorkspaceBloc>().state.userProfile;
|
||||
final userProfile = context.read<UserWorkspaceBloc>().userProfile;
|
||||
final workspaceId = state.currentWorkspace?.workspaceId ?? '';
|
||||
return MultiBlocProvider(
|
||||
providers: [
|
||||
|
||||
@@ -60,7 +60,7 @@ class _PromptInputDesktopSelectSourcesButtonState
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final userWorkspaceBloc = context.read<UserWorkspaceBloc>();
|
||||
final userProfile = userWorkspaceBloc.state.userProfile;
|
||||
final userProfile = userWorkspaceBloc.userProfile;
|
||||
final workspaceId =
|
||||
userWorkspaceBloc.state.currentWorkspace?.workspaceId ?? '';
|
||||
|
||||
|
||||
@@ -39,35 +39,22 @@ Future<bool> afLaunchUri(
|
||||
);
|
||||
}
|
||||
|
||||
// on Linux or Android or Windows, add http scheme to the url if it is not present
|
||||
if ((UniversalPlatform.isLinux ||
|
||||
UniversalPlatform.isAndroid ||
|
||||
UniversalPlatform.isWindows) &&
|
||||
!isURL(url, {'require_protocol': true})) {
|
||||
// on Linux, add http scheme to the url if it is not present
|
||||
if (UniversalPlatform.isLinux && !isURL(url, {'require_protocol': true})) {
|
||||
uri = Uri.parse('https://$url');
|
||||
}
|
||||
|
||||
/// opening an incorrect link will cause a system error dialog to pop up on macOS
|
||||
/// only use [canLaunchUrl] on macOS
|
||||
/// and there is an known issue with url_launcher on Linux where it fails to launch
|
||||
/// see https://github.com/flutter/flutter/issues/88463
|
||||
bool result = true;
|
||||
if (UniversalPlatform.isMacOS) {
|
||||
result = await launcher.canLaunchUrl(uri);
|
||||
}
|
||||
|
||||
if (result) {
|
||||
try {
|
||||
// try to launch the uri directly
|
||||
result = await launcher.launchUrl(
|
||||
uri,
|
||||
mode: mode,
|
||||
webOnlyWindowName: webOnlyWindowName,
|
||||
);
|
||||
} on PlatformException catch (e) {
|
||||
Log.error('Failed to open uri: $e');
|
||||
return false;
|
||||
}
|
||||
// try to launch the uri directly
|
||||
bool result;
|
||||
try {
|
||||
result = await launcher.launchUrl(
|
||||
uri,
|
||||
mode: mode,
|
||||
webOnlyWindowName: webOnlyWindowName,
|
||||
);
|
||||
} on PlatformException catch (e) {
|
||||
Log.error('Failed to open uri: $e');
|
||||
return false;
|
||||
}
|
||||
|
||||
// if the uri is not a valid url, try to launch it with http scheme
|
||||
|
||||
@@ -16,6 +16,7 @@ class AppFlowyConfiguration {
|
||||
required this.platform,
|
||||
required this.authenticator_type,
|
||||
required this.appflowy_cloud_config,
|
||||
required this.is_anon,
|
||||
required this.envs,
|
||||
});
|
||||
|
||||
@@ -28,6 +29,7 @@ class AppFlowyConfiguration {
|
||||
final String origin_app_path;
|
||||
final String device_id;
|
||||
final String platform;
|
||||
final bool is_anon;
|
||||
final int authenticator_type;
|
||||
final AppFlowyCloudConfiguration appflowy_cloud_config;
|
||||
final Map<String, String> envs;
|
||||
|
||||
+2
-27
@@ -23,9 +23,6 @@ import 'package:appflowy_backend/log.dart';
|
||||
///
|
||||
Future<void> _setAuthenticatorType(AuthenticatorType ty) async {
|
||||
switch (ty) {
|
||||
case AuthenticatorType.local:
|
||||
await getIt<KeyValueStorage>().set(KVKeys.kCloudType, 0.toString());
|
||||
break;
|
||||
case AuthenticatorType.appflowyCloud:
|
||||
await getIt<KeyValueStorage>().set(KVKeys.kCloudType, 2.toString());
|
||||
break;
|
||||
@@ -63,8 +60,6 @@ Future<AuthenticatorType> getAuthenticatorType() async {
|
||||
}
|
||||
|
||||
switch (value ?? "0") {
|
||||
case "0":
|
||||
return AuthenticatorType.local;
|
||||
case "2":
|
||||
return AuthenticatorType.appflowyCloud;
|
||||
case "3":
|
||||
@@ -100,24 +95,17 @@ bool get isAuthEnabled {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool get isLocalAuthEnabled {
|
||||
return currentCloudType().isLocal;
|
||||
}
|
||||
|
||||
/// Determines if AppFlowy Cloud is enabled.
|
||||
bool get isAppFlowyCloudEnabled {
|
||||
return currentCloudType().isAppFlowyCloudEnabled;
|
||||
}
|
||||
|
||||
enum AuthenticatorType {
|
||||
local,
|
||||
appflowyCloud,
|
||||
appflowyCloudSelfHost,
|
||||
// The 'appflowyCloudDevelop' type is used for develop purposes only.
|
||||
appflowyCloudDevelop;
|
||||
|
||||
bool get isLocal => this == AuthenticatorType.local;
|
||||
|
||||
bool get isAppFlowyCloudEnabled =>
|
||||
this == AuthenticatorType.appflowyCloudSelfHost ||
|
||||
this == AuthenticatorType.appflowyCloudDevelop ||
|
||||
@@ -125,8 +113,6 @@ enum AuthenticatorType {
|
||||
|
||||
int get value {
|
||||
switch (this) {
|
||||
case AuthenticatorType.local:
|
||||
return 0;
|
||||
case AuthenticatorType.appflowyCloud:
|
||||
return 2;
|
||||
case AuthenticatorType.appflowyCloudSelfHost:
|
||||
@@ -138,8 +124,6 @@ enum AuthenticatorType {
|
||||
|
||||
static AuthenticatorType fromValue(int value) {
|
||||
switch (value) {
|
||||
case 0:
|
||||
return AuthenticatorType.local;
|
||||
case 2:
|
||||
return AuthenticatorType.appflowyCloud;
|
||||
case 3:
|
||||
@@ -147,7 +131,7 @@ enum AuthenticatorType {
|
||||
case 4:
|
||||
return AuthenticatorType.appflowyCloudDevelop;
|
||||
default:
|
||||
return AuthenticatorType.local;
|
||||
return AuthenticatorType.appflowyCloud;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -167,16 +151,11 @@ Future<void> useBaseWebDomain(String? url) async {
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> useSelfHostedAppFlowyCloud(String url) async {
|
||||
Future<void> useSelfHostedAppFlowyCloudWithURL(String url) async {
|
||||
await _setAuthenticatorType(AuthenticatorType.appflowyCloudSelfHost);
|
||||
await _setAppFlowyCloudUrl(url);
|
||||
}
|
||||
|
||||
Future<void> useAppFlowyCloudDevelop(String url) async {
|
||||
await _setAuthenticatorType(AuthenticatorType.appflowyCloudDevelop);
|
||||
await _setAppFlowyCloudUrl(url);
|
||||
}
|
||||
|
||||
Future<void> useAppFlowyBetaCloudWithURL(
|
||||
String url,
|
||||
AuthenticatorType authenticatorType,
|
||||
@@ -185,10 +164,6 @@ Future<void> useAppFlowyBetaCloudWithURL(
|
||||
await _setAppFlowyCloudUrl(url);
|
||||
}
|
||||
|
||||
Future<void> useLocalServer() async {
|
||||
await _setAuthenticatorType(AuthenticatorType.local);
|
||||
}
|
||||
|
||||
// Use getIt<AppFlowyCloudSharedEnv>() to get the shared environment.
|
||||
class AppFlowyCloudSharedEnv {
|
||||
AppFlowyCloudSharedEnv({
|
||||
|
||||
+17
-19
@@ -30,7 +30,7 @@ class NotificationReminderBloc
|
||||
add(const NotificationReminderEvent.reset());
|
||||
},
|
||||
reset: () async {
|
||||
final scheduledAt = await _getScheduledAt(
|
||||
final createdAt = await _getCreatedAt(
|
||||
reminder,
|
||||
dateFormat,
|
||||
timeFormat,
|
||||
@@ -40,26 +40,23 @@ class NotificationReminderBloc
|
||||
if (view == null) {
|
||||
emit(
|
||||
NotificationReminderState(
|
||||
scheduledAt: scheduledAt,
|
||||
createdAt: createdAt,
|
||||
pageTitle: '',
|
||||
reminderContent: '',
|
||||
isLocked: false,
|
||||
status: NotificationReminderStatus.error,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final layout = view.layout;
|
||||
final layout = view!.layout;
|
||||
|
||||
if (layout.isDocumentView) {
|
||||
final node = await _getContent(reminder);
|
||||
if (node != null) {
|
||||
emit(
|
||||
NotificationReminderState(
|
||||
scheduledAt: scheduledAt,
|
||||
createdAt: createdAt,
|
||||
pageTitle: view.nameOrDefault,
|
||||
isLocked: view.isLocked,
|
||||
view: view,
|
||||
reminderContent: node.delta?.toPlainText() ?? '',
|
||||
nodes: [node],
|
||||
@@ -71,9 +68,8 @@ class NotificationReminderBloc
|
||||
} else if (layout.isDatabaseView) {
|
||||
emit(
|
||||
NotificationReminderState(
|
||||
scheduledAt: scheduledAt,
|
||||
createdAt: createdAt,
|
||||
pageTitle: view.nameOrDefault,
|
||||
isLocked: view.isLocked,
|
||||
view: view,
|
||||
reminderContent: reminder.message,
|
||||
status: NotificationReminderStatus.loaded,
|
||||
@@ -89,16 +85,20 @@ class NotificationReminderBloc
|
||||
late final UserDateFormatPB dateFormat;
|
||||
late final UserTimeFormatPB timeFormat;
|
||||
|
||||
Future<String> _getScheduledAt(
|
||||
Future<String> _getCreatedAt(
|
||||
ReminderPB reminder,
|
||||
UserDateFormatPB dateFormat,
|
||||
UserTimeFormatPB timeFormat,
|
||||
) async {
|
||||
return _formatTimestamp(
|
||||
reminder.scheduledAt.toInt() * 1000,
|
||||
timeFormat: timeFormat,
|
||||
dateFormate: dateFormat,
|
||||
);
|
||||
final rCreatedAt = reminder.createdAt;
|
||||
final createdAt = rCreatedAt != null
|
||||
? _formatTimestamp(
|
||||
rCreatedAt,
|
||||
timeFormat: timeFormat,
|
||||
dateFormate: dateFormat,
|
||||
)
|
||||
: '';
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
Future<ViewPB?> _getView(ReminderPB reminder) async {
|
||||
@@ -200,10 +200,9 @@ class NotificationReminderState with _$NotificationReminderState {
|
||||
const NotificationReminderState._();
|
||||
|
||||
const factory NotificationReminderState({
|
||||
required String scheduledAt,
|
||||
required String createdAt,
|
||||
required String pageTitle,
|
||||
required String reminderContent,
|
||||
required bool isLocked,
|
||||
@Default(NotificationReminderStatus.initial)
|
||||
NotificationReminderStatus status,
|
||||
@Default([]) List<Node> nodes,
|
||||
@@ -213,9 +212,8 @@ class NotificationReminderState with _$NotificationReminderState {
|
||||
|
||||
factory NotificationReminderState.initial() =>
|
||||
const NotificationReminderState(
|
||||
scheduledAt: '',
|
||||
createdAt: '',
|
||||
pageTitle: '',
|
||||
reminderContent: '',
|
||||
isLocked: false,
|
||||
);
|
||||
}
|
||||
+7
-6
@@ -19,13 +19,14 @@ class UserProfileBloc extends Bloc<UserProfileEvent, UserProfileState> {
|
||||
|
||||
Future<void> _initialize(Emitter<UserProfileState> emit) async {
|
||||
emit(const UserProfileState.loading());
|
||||
final latestOrFailure =
|
||||
|
||||
final workspaceOrFailure =
|
||||
await FolderEventGetCurrentWorkspaceSetting().send();
|
||||
|
||||
final userOrFailure = await getIt<AuthService>().getUser();
|
||||
|
||||
final latest = latestOrFailure.fold(
|
||||
(latestPB) => latestPB,
|
||||
final workspaceSetting = workspaceOrFailure.fold(
|
||||
(workspaceSettingPB) => workspaceSettingPB,
|
||||
(error) => null,
|
||||
);
|
||||
|
||||
@@ -34,13 +35,13 @@ class UserProfileBloc extends Bloc<UserProfileEvent, UserProfileState> {
|
||||
(error) => null,
|
||||
);
|
||||
|
||||
if (latest == null || userProfile == null) {
|
||||
if (workspaceSetting == null || userProfile == null) {
|
||||
return emit(const UserProfileState.workspaceFailure());
|
||||
}
|
||||
|
||||
emit(
|
||||
UserProfileState.success(
|
||||
workspaceSettings: latest,
|
||||
workspaceSettings: workspaceSetting,
|
||||
userProfile: userProfile,
|
||||
),
|
||||
);
|
||||
@@ -58,7 +59,7 @@ class UserProfileState with _$UserProfileState {
|
||||
const factory UserProfileState.loading() = _Loading;
|
||||
const factory UserProfileState.workspaceFailure() = _WorkspaceFailure;
|
||||
const factory UserProfileState.success({
|
||||
required WorkspaceLatestPB workspaceSettings,
|
||||
required WorkspaceSettingPB workspaceSettings,
|
||||
required UserProfilePB userProfile,
|
||||
}) = _Success;
|
||||
}
|
||||
+1
-1
@@ -66,7 +66,7 @@ class MobileViewPageMoreBottomSheet extends StatelessWidget {
|
||||
break;
|
||||
case MobileViewBottomSheetBodyAction.delete:
|
||||
context.read<ViewBloc>().add(const ViewEvent.delete());
|
||||
Navigator.of(context).pop();
|
||||
context.pop();
|
||||
break;
|
||||
case MobileViewBottomSheetBodyAction.addToFavorites:
|
||||
_addFavorite(context);
|
||||
|
||||
+57
-369
@@ -1,37 +1,22 @@
|
||||
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_header.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_edit_menu.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_search_text_field.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_styles.dart';
|
||||
import 'package:appflowy/plugins/shared/share/constants.dart';
|
||||
import 'package:appflowy/user/application/user_service.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_service.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:appflowy_result/appflowy_result.dart';
|
||||
import 'package:appflowy_ui/appflowy_ui.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
// ignore: implementation_imports
|
||||
import 'package:appflowy_editor/src/editor/util/link_util.dart';
|
||||
|
||||
class MobileBottomSheetEditLinkWidget extends StatefulWidget {
|
||||
const MobileBottomSheetEditLinkWidget({
|
||||
super.key,
|
||||
required this.linkInfo,
|
||||
required this.onApply,
|
||||
required this.onRemoveLink,
|
||||
required this.currentViewId,
|
||||
required this.onDispose,
|
||||
required this.text,
|
||||
required this.href,
|
||||
required this.onEdit,
|
||||
});
|
||||
|
||||
final LinkInfo linkInfo;
|
||||
final ValueChanged<LinkInfo> onApply;
|
||||
final ValueChanged<LinkInfo> onRemoveLink;
|
||||
final VoidCallback onDispose;
|
||||
final String currentViewId;
|
||||
final String text;
|
||||
final String? href;
|
||||
final void Function(String text, String href) onEdit;
|
||||
|
||||
@override
|
||||
State<MobileBottomSheetEditLinkWidget> createState() =>
|
||||
@@ -40,377 +25,80 @@ class MobileBottomSheetEditLinkWidget extends StatefulWidget {
|
||||
|
||||
class _MobileBottomSheetEditLinkWidgetState
|
||||
extends State<MobileBottomSheetEditLinkWidget> {
|
||||
ValueChanged<LinkInfo> get onApply => widget.onApply;
|
||||
|
||||
ValueChanged<LinkInfo> get onRemoveLink => widget.onRemoveLink;
|
||||
|
||||
late TextEditingController linkNameController =
|
||||
TextEditingController(text: linkInfo.name);
|
||||
final textFocusNode = FocusNode();
|
||||
late LinkInfo linkInfo = widget.linkInfo;
|
||||
late LinkSearchTextField searchTextField;
|
||||
bool isShowingSearchResult = false;
|
||||
ViewPB? currentView;
|
||||
bool showErrorText = false;
|
||||
bool showRemoveLink = false;
|
||||
String title = LocaleKeys.editor_editLink.tr();
|
||||
|
||||
AppFlowyThemeData get theme => AppFlowyTheme.of(context);
|
||||
late final TextEditingController textController;
|
||||
late final TextEditingController hrefController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final isPageLink = linkInfo.isPage;
|
||||
if (isPageLink) getPageView();
|
||||
searchTextField = LinkSearchTextField(
|
||||
initialSearchText: isPageLink ? '' : linkInfo.link,
|
||||
initialViewId: linkInfo.viewId,
|
||||
currentViewId: widget.currentViewId,
|
||||
onEnter: () {},
|
||||
onEscape: () {},
|
||||
onDataRefresh: () {
|
||||
if (mounted) setState(() {});
|
||||
},
|
||||
)..searchRecentViews();
|
||||
if (linkInfo.link.isEmpty) {
|
||||
isShowingSearchResult = true;
|
||||
title = LocaleKeys.toolbar_addLink.tr();
|
||||
} else {
|
||||
showRemoveLink = true;
|
||||
textFocusNode.requestFocus();
|
||||
}
|
||||
textFocusNode.addListener(() {
|
||||
if (!mounted) return;
|
||||
if (textFocusNode.hasFocus) {
|
||||
setState(() {
|
||||
isShowingSearchResult = false;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
textController = TextEditingController(
|
||||
text: widget.text,
|
||||
);
|
||||
hrefController = TextEditingController(
|
||||
text: widget.href,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
linkNameController.dispose();
|
||||
textFocusNode.dispose();
|
||||
searchTextField.dispose();
|
||||
widget.onDispose();
|
||||
textController.dispose();
|
||||
hrefController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
height: MediaQuery.of(context).size.height * 0.8,
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
BottomSheetHeader(
|
||||
title: title,
|
||||
onClose: () => context.pop(),
|
||||
confirmButton: FlowyTextButton(
|
||||
LocaleKeys.button_done.tr(),
|
||||
constraints:
|
||||
const BoxConstraints.tightFor(width: 62, height: 30),
|
||||
padding: const EdgeInsets.only(left: 12),
|
||||
fontColor: theme.textColorScheme.onFill,
|
||||
fillColor: Theme.of(context).primaryColor,
|
||||
onPressed: () {
|
||||
if (isShowingSearchResult) {
|
||||
onConfirm();
|
||||
return;
|
||||
}
|
||||
if (linkInfo.link.isEmpty || !isUri(linkInfo.link)) {
|
||||
setState(() {
|
||||
showErrorText = true;
|
||||
});
|
||||
return;
|
||||
}
|
||||
widget.onApply.call(linkInfo);
|
||||
context.pop();
|
||||
},
|
||||
),
|
||||
),
|
||||
const VSpace(20.0),
|
||||
buildNameTextField(),
|
||||
const VSpace(16.0),
|
||||
buildLinkField(),
|
||||
const VSpace(20.0),
|
||||
buildRemoveLink(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildNameTextField() {
|
||||
return SizedBox(
|
||||
height: 48,
|
||||
child: TextFormField(
|
||||
focusNode: textFocusNode,
|
||||
textAlign: TextAlign.left,
|
||||
controller: linkNameController,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
height: 20 / 16,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
onChanged: (text) {
|
||||
linkInfo = LinkInfo(
|
||||
name: text,
|
||||
link: linkInfo.link,
|
||||
isPage: linkInfo.isPage,
|
||||
);
|
||||
},
|
||||
decoration: LinkStyle.buildLinkTextFieldInputDecoration(
|
||||
LocaleKeys.document_toolbar_linkNameHint.tr(),
|
||||
contentPadding: EdgeInsets.all(14),
|
||||
radius: 12,
|
||||
context,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildLinkField() {
|
||||
final width = MediaQuery.of(context).size.width;
|
||||
final showPageView = linkInfo.isPage && !isShowingSearchResult;
|
||||
Widget child;
|
||||
if (showPageView) {
|
||||
child = buildPageView();
|
||||
} else if (!isShowingSearchResult) {
|
||||
child = buildLinkView();
|
||||
} else {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
searchTextField.buildTextField(
|
||||
autofocus: true,
|
||||
context: context,
|
||||
contentPadding: EdgeInsets.all(14),
|
||||
textStyle: TextStyle(
|
||||
fontSize: 16,
|
||||
height: 20 / 16,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
),
|
||||
VSpace(6),
|
||||
searchTextField.buildResultContainer(
|
||||
context: context,
|
||||
onPageLinkSelected: onPageSelected,
|
||||
onLinkSelected: onLinkSelected,
|
||||
width: width - 32,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
child,
|
||||
if (showErrorText)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 4),
|
||||
child: FlowyText.regular(
|
||||
LocaleKeys.document_plugins_file_networkUrlInvalid.tr(),
|
||||
color: theme.textColorScheme.error,
|
||||
fontSize: 12,
|
||||
figmaLineHeight: 16,
|
||||
),
|
||||
),
|
||||
BottomSheetHeader(
|
||||
title: LocaleKeys.editor_editLink.tr(),
|
||||
onClose: () => context.pop(),
|
||||
onDone: () {
|
||||
widget.onEdit(textController.text, hrefController.text);
|
||||
},
|
||||
),
|
||||
const VSpace(20.0),
|
||||
_buildTextField(
|
||||
textController,
|
||||
LocaleKeys.document_inlineLink_title_placeholder.tr(),
|
||||
),
|
||||
const VSpace(12.0),
|
||||
_buildTextField(
|
||||
hrefController,
|
||||
LocaleKeys.document_inlineLink_url_placeholder.tr(),
|
||||
),
|
||||
const VSpace(12.0),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildPageView() {
|
||||
final height = 48.0;
|
||||
late Widget child;
|
||||
final view = currentView;
|
||||
if (view == null) {
|
||||
child = Center(
|
||||
child: SizedBox.fromSize(
|
||||
size: Size(10, 10),
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
final viewName = view.name;
|
||||
final displayName = viewName.isEmpty
|
||||
? LocaleKeys.document_title_placeholder.tr()
|
||||
: viewName;
|
||||
child = GestureDetector(
|
||||
onTap: showSearchResult,
|
||||
child: Container(
|
||||
height: height,
|
||||
color: Colors.grey.withAlpha(1),
|
||||
padding: EdgeInsets.all(14),
|
||||
child: Row(
|
||||
children: [
|
||||
searchTextField.buildIcon(view),
|
||||
HSpace(4),
|
||||
Flexible(
|
||||
child: FlowyText.regular(
|
||||
displayName,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
figmaLineHeight: 20,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return Container(
|
||||
height: height,
|
||||
decoration: buildBorderDecoration(),
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildLinkView() {
|
||||
return Container(
|
||||
height: 48,
|
||||
decoration: buildBorderDecoration(),
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: showSearchResult,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(12),
|
||||
child: Row(
|
||||
children: [
|
||||
FlowySvg(FlowySvgs.toolbar_link_earth_m),
|
||||
HSpace(8),
|
||||
Flexible(
|
||||
child: FlowyText.regular(
|
||||
linkInfo.link,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
figmaLineHeight: 20,
|
||||
),
|
||||
),
|
||||
],
|
||||
Widget _buildTextField(
|
||||
TextEditingController controller,
|
||||
String? hintText,
|
||||
) {
|
||||
return SizedBox(
|
||||
height: 48.0,
|
||||
child: FlowyTextField(
|
||||
controller: controller,
|
||||
hintText: hintText,
|
||||
textStyle: const TextStyle(fontSize: 16.0),
|
||||
hintStyle: const TextStyle(fontSize: 16.0),
|
||||
suffixIcon: Padding(
|
||||
padding: const EdgeInsets.all(4.0),
|
||||
child: FlowyButton(
|
||||
text: const FlowySvg(
|
||||
FlowySvgs.close_lg,
|
||||
),
|
||||
margin: EdgeInsets.zero,
|
||||
useIntrinsicWidth: true,
|
||||
onTap: () {
|
||||
controller.clear();
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildRemoveLink() {
|
||||
if (!showRemoveLink) return SizedBox.shrink();
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () {
|
||||
widget.onRemoveLink(linkInfo);
|
||||
context.pop();
|
||||
},
|
||||
child: SizedBox(
|
||||
height: 32,
|
||||
child: Center(
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
FlowySvg(
|
||||
FlowySvgs.mobile_icon_remove_link_m,
|
||||
color: theme.iconColorScheme.secondary,
|
||||
),
|
||||
HSpace(8),
|
||||
FlowyText.regular(
|
||||
LocaleKeys.editor_removeLink.tr(),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
figmaLineHeight: 20,
|
||||
color: theme.textColorScheme.secondary,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void onConfirm() {
|
||||
searchTextField.onSearchResult(
|
||||
onLink: onLinkSelected,
|
||||
onRecentViews: () => onPageSelected(searchTextField.currentRecentView),
|
||||
onSearchViews: () => onPageSelected(searchTextField.currentSearchedView),
|
||||
onEmpty: () {
|
||||
searchTextField.unfocus();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> onPageSelected(ViewPB view) async {
|
||||
currentView = view;
|
||||
final link = ShareConstants.buildShareUrl(
|
||||
workspaceId: await UserBackendService.getCurrentWorkspace().fold(
|
||||
(s) => s.id,
|
||||
(f) => '',
|
||||
),
|
||||
viewId: view.id,
|
||||
);
|
||||
linkInfo = LinkInfo(
|
||||
name: linkInfo.name,
|
||||
link: link,
|
||||
isPage: true,
|
||||
);
|
||||
searchTextField.updateText(linkInfo.link);
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
isShowingSearchResult = false;
|
||||
searchTextField.unfocus();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void onLinkSelected() {
|
||||
if (mounted) {
|
||||
linkInfo = LinkInfo(
|
||||
name: linkInfo.name,
|
||||
link: searchTextField.searchText,
|
||||
);
|
||||
hideSearchResult();
|
||||
}
|
||||
}
|
||||
|
||||
void hideSearchResult() {
|
||||
setState(() {
|
||||
isShowingSearchResult = false;
|
||||
searchTextField.unfocus();
|
||||
textFocusNode.unfocus();
|
||||
});
|
||||
}
|
||||
|
||||
void showSearchResult() {
|
||||
setState(() {
|
||||
if (linkInfo.isPage) searchTextField.updateText('');
|
||||
isShowingSearchResult = true;
|
||||
searchTextField.requestFocus();
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> getPageView() async {
|
||||
if (!linkInfo.isPage) return;
|
||||
final (view, isInTrash, isDeleted) =
|
||||
await ViewBackendService.getMentionPageStatus(linkInfo.viewId);
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
currentView = view;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
BoxDecoration buildCardDecoration() {
|
||||
return BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(theme.borderRadius.l),
|
||||
boxShadow: theme.shadow.medium,
|
||||
);
|
||||
}
|
||||
|
||||
BoxDecoration buildBorderDecoration() {
|
||||
return BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(theme.borderRadius.l),
|
||||
border: Border.all(color: theme.borderColorScheme.greyPrimary),
|
||||
);
|
||||
}
|
||||
}
|
||||
+4
-4
@@ -8,13 +8,11 @@ class BottomSheetHeader extends StatelessWidget {
|
||||
this.title,
|
||||
this.onClose,
|
||||
this.onDone,
|
||||
this.confirmButton,
|
||||
});
|
||||
|
||||
final String? title;
|
||||
final VoidCallback? onClose;
|
||||
final VoidCallback? onDone;
|
||||
final Widget? confirmButton;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -38,10 +36,12 @@ class BottomSheetHeader extends StatelessWidget {
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
if (onDone != null || confirmButton != null)
|
||||
if (onDone != null)
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: confirmButton ?? BottomSheetDoneButton(onDone: onDone),
|
||||
child: BottomSheetDoneButton(
|
||||
onDone: onDone,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
+2
-4
@@ -182,7 +182,7 @@ class MobileViewBottomSheetBody extends StatelessWidget {
|
||||
),
|
||||
_divider(),
|
||||
..._buildPublishActions(context),
|
||||
|
||||
_divider(),
|
||||
MobileQuickActionButton(
|
||||
text: LocaleKeys.button_delete.tr(),
|
||||
textColor: Theme.of(context).colorScheme.error,
|
||||
@@ -203,7 +203,7 @@ class MobileViewBottomSheetBody extends StatelessWidget {
|
||||
final userProfile = context.read<MobileViewPageBloc>().state.userProfilePB;
|
||||
// the publish feature is only available for AppFlowy Cloud
|
||||
if (userProfile == null ||
|
||||
userProfile.workspaceType != WorkspaceTypePB.ServerW) {
|
||||
userProfile.authenticator != AuthenticatorPB.AppFlowyCloud) {
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -236,7 +236,6 @@ class MobileViewBottomSheetBody extends StatelessWidget {
|
||||
MobileViewBottomSheetBodyAction.unpublish,
|
||||
),
|
||||
),
|
||||
_divider(),
|
||||
];
|
||||
} else {
|
||||
return [
|
||||
@@ -247,7 +246,6 @@ class MobileViewBottomSheetBody extends StatelessWidget {
|
||||
MobileViewBottomSheetBodyAction.publish,
|
||||
),
|
||||
),
|
||||
_divider(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
+4
-6
@@ -1,6 +1,5 @@
|
||||
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet_buttons.dart';
|
||||
import 'package:appflowy/plugins/base/drag_handler.dart';
|
||||
import 'package:appflowy_ui/appflowy_ui.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
@@ -236,7 +235,6 @@ class BottomSheetHeader extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = AppFlowyTheme.of(context);
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 4.0),
|
||||
child: SizedBox(
|
||||
@@ -267,11 +265,11 @@ class BottomSheetHeader extends StatelessWidget {
|
||||
Align(
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(maxWidth: 250),
|
||||
child: Text(
|
||||
child: FlowyText(
|
||||
title,
|
||||
style: theme.textStyle.heading4.prominent(
|
||||
color: theme.textColorScheme.primary,
|
||||
),
|
||||
fontSize: 17.0,
|
||||
fontWeight: FontWeight.w500,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
+4
-4
@@ -31,9 +31,9 @@ class MobileFavoriteScreen extends StatelessWidget {
|
||||
return const Center(child: CircularProgressIndicator.adaptive());
|
||||
}
|
||||
|
||||
final latest = snapshots.data?[0].fold(
|
||||
(latest) {
|
||||
return latest as WorkspaceLatestPB?;
|
||||
final workspaceSetting = snapshots.data?[0].fold(
|
||||
(workspaceSettingPB) {
|
||||
return workspaceSettingPB as WorkspaceSettingPB?;
|
||||
},
|
||||
(error) => null,
|
||||
);
|
||||
@@ -46,7 +46,7 @@ class MobileFavoriteScreen extends StatelessWidget {
|
||||
|
||||
// In the unlikely case either of the above is null, eg.
|
||||
// when a workspace is already open this can happen.
|
||||
if (latest == null || userProfile == null) {
|
||||
if (workspaceSetting == null || userProfile == null) {
|
||||
return const WorkspaceFailedScreen();
|
||||
}
|
||||
|
||||
|
||||
@@ -2,12 +2,10 @@ import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/mobile/presentation/home/mobile_home_page_header.dart';
|
||||
import 'package:appflowy/mobile/presentation/home/tab/mobile_space_tab.dart';
|
||||
import 'package:appflowy/mobile/presentation/home/tab/space_order_bloc.dart';
|
||||
import 'package:appflowy/shared/feature_flags.dart';
|
||||
import 'package:appflowy/shared/loading.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/user/application/auth/auth_service.dart';
|
||||
import 'package:appflowy/user/application/reminder/reminder_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/command_palette/command_palette_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/menu/sidebar_sections_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/recent/cached_recent_service.dart';
|
||||
@@ -27,6 +25,7 @@ import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:sentry/sentry.dart';
|
||||
|
||||
class MobileHomeScreen extends StatelessWidget {
|
||||
const MobileHomeScreen({super.key});
|
||||
@@ -45,9 +44,9 @@ class MobileHomeScreen extends StatelessWidget {
|
||||
return const Center(child: CircularProgressIndicator.adaptive());
|
||||
}
|
||||
|
||||
final workspaceLatest = snapshots.data?[0].fold(
|
||||
(workspaceLatestPB) {
|
||||
return workspaceLatestPB as WorkspaceLatestPB?;
|
||||
final workspaceSetting = snapshots.data?[0].fold(
|
||||
(workspaceSettingPB) {
|
||||
return workspaceSettingPB as WorkspaceSettingPB?;
|
||||
},
|
||||
(error) => null,
|
||||
);
|
||||
@@ -60,10 +59,18 @@ class MobileHomeScreen extends StatelessWidget {
|
||||
|
||||
// In the unlikely case either of the above is null, eg.
|
||||
// when a workspace is already open this can happen.
|
||||
if (workspaceLatest == null || userProfile == null) {
|
||||
if (workspaceSetting == null || userProfile == null) {
|
||||
return const WorkspaceFailedScreen();
|
||||
}
|
||||
|
||||
Sentry.configureScope(
|
||||
(scope) => scope.setUser(
|
||||
SentryUser(
|
||||
id: userProfile.id.toString(),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
return Scaffold(
|
||||
body: SafeArea(
|
||||
bottom: false,
|
||||
@@ -71,7 +78,7 @@ class MobileHomeScreen extends StatelessWidget {
|
||||
value: userProfile,
|
||||
child: MobileHomePage(
|
||||
userProfile: userProfile,
|
||||
workspaceLatest: workspaceLatest,
|
||||
workspaceSetting: workspaceSetting,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -88,11 +95,11 @@ class MobileHomePage extends StatefulWidget {
|
||||
const MobileHomePage({
|
||||
super.key,
|
||||
required this.userProfile,
|
||||
required this.workspaceLatest,
|
||||
required this.workspaceSetting,
|
||||
});
|
||||
|
||||
final UserProfilePB userProfile;
|
||||
final WorkspaceLatestPB workspaceLatest;
|
||||
final WorkspaceSettingPB workspaceSetting;
|
||||
|
||||
@override
|
||||
State<MobileHomePage> createState() => _MobileHomePageState();
|
||||
@@ -138,7 +145,7 @@ class _MobileHomePageState extends State<MobileHomePage> {
|
||||
|
||||
void _onLatestViewChange() async {
|
||||
final id = getIt<MenuSharedState>().latestOpenView?.id;
|
||||
if (id == null || id.isEmpty) {
|
||||
if (id == null) {
|
||||
return;
|
||||
}
|
||||
await FolderEventSetLatestView(ViewIdPB(value: id)).send();
|
||||
@@ -166,14 +173,7 @@ class _HomePageState extends State<_HomePage> {
|
||||
listener: (context, state) {
|
||||
getIt<CachedRecentService>().reset();
|
||||
mCurrentWorkspace.value = state.currentWorkspace;
|
||||
if (FeatureFlag.search.isOn) {
|
||||
// Notify command palette that workspace has changed
|
||||
context.read<CommandPaletteBloc>().add(
|
||||
CommandPaletteEvent.workspaceChanged(
|
||||
workspaceId: state.currentWorkspace?.workspaceId,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Debounce.debounce(
|
||||
'workspace_action_result',
|
||||
const Duration(milliseconds: 150),
|
||||
|
||||
@@ -194,7 +194,6 @@ class _MobileWorkspace extends StatelessWidget {
|
||||
context.read<UserWorkspaceBloc>().add(
|
||||
UserWorkspaceEvent.openWorkspace(
|
||||
workspace.workspaceId,
|
||||
workspace.workspaceType,
|
||||
),
|
||||
);
|
||||
},
|
||||
|
||||
+8
-3
@@ -71,6 +71,12 @@ class _MobileHomeSettingPageState extends State<MobileHomeSettingPage> {
|
||||
}
|
||||
|
||||
Widget _buildSettingsWidget(UserProfilePB userProfile) {
|
||||
// show the third-party sign in buttons if user logged in with local session and auth is enabled.
|
||||
|
||||
final isLocalAuthEnabled =
|
||||
userProfile.authenticator == AuthenticatorPB.Local && isAuthEnabled;
|
||||
'';
|
||||
|
||||
return BlocProvider(
|
||||
create: (context) => UserWorkspaceBloc(userProfile: userProfile)
|
||||
..add(const UserWorkspaceEvent.initial()),
|
||||
@@ -85,8 +91,7 @@ class _MobileHomeSettingPageState extends State<MobileHomeSettingPage> {
|
||||
PersonalInfoSettingGroup(
|
||||
userProfile: userProfile,
|
||||
),
|
||||
if (state.userProfile.userAuthType == AuthTypePB.Server)
|
||||
const WorkspaceSettingGroup(),
|
||||
const WorkspaceSettingGroup(),
|
||||
const AppearanceSettingGroup(),
|
||||
const LanguageSettingGroup(),
|
||||
if (Env.enableCustomCloud) const CloudSettingGroup(),
|
||||
@@ -100,7 +105,7 @@ class _MobileHomeSettingPageState extends State<MobileHomeSettingPage> {
|
||||
const AboutSettingGroup(),
|
||||
UserSessionSettingGroup(
|
||||
userProfile: userProfile,
|
||||
showThirdPartyLogin: false,
|
||||
showThirdPartyLogin: isLocalAuthEnabled,
|
||||
),
|
||||
const VSpace(20),
|
||||
],
|
||||
|
||||
+1
-1
@@ -48,7 +48,7 @@ class HomePageSettingsPopupMenu extends StatelessWidget {
|
||||
text: LocaleKeys.settings_popupMenuItem_settings.tr(),
|
||||
),
|
||||
// only show the member items in cloud mode
|
||||
if (userProfile.workspaceType == WorkspaceTypePB.ServerW) ...[
|
||||
if (userProfile.authenticator == AuthenticatorPB.AppFlowyCloud) ...[
|
||||
const PopupMenuDivider(height: 0.5),
|
||||
_buildItem(
|
||||
value: _MobileSettingsPopupMenuItem.members,
|
||||
|
||||
@@ -200,9 +200,7 @@ class MobileViewPage extends StatelessWidget {
|
||||
const WidgetSpan(child: HSpace(8.0)),
|
||||
],
|
||||
TextSpan(
|
||||
text: name.orDefault(
|
||||
LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
|
||||
),
|
||||
text: name,
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
fontSize: 16.0,
|
||||
fontWeight: FontWeight.w600,
|
||||
|
||||
@@ -3,7 +3,6 @@ import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/mobile/presentation/base/animated_gesture.dart';
|
||||
import 'package:appflowy/mobile/presentation/home/tab/mobile_space_tab.dart';
|
||||
import 'package:appflowy/util/theme_extension.dart';
|
||||
import 'package:appflowy_ui/appflowy_ui.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -83,35 +82,3 @@ class FloatingAIEntry extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class FloatingAIEntryV2 extends StatelessWidget {
|
||||
const FloatingAIEntryV2({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = AppFlowyTheme.of(context);
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
mobileCreateNewAIChatNotifier.value =
|
||||
mobileCreateNewAIChatNotifier.value + 1;
|
||||
},
|
||||
child: Container(
|
||||
width: 56,
|
||||
height: 56,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: theme.surfaceColorScheme.primary,
|
||||
boxShadow: theme.shadow.small,
|
||||
border: Border.all(color: theme.borderColorScheme.primary),
|
||||
),
|
||||
child: Center(
|
||||
child: FlowySvg(
|
||||
FlowySvgs.m_home_ai_chat_icon_m,
|
||||
blendMode: null,
|
||||
size: Size(24, 24),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -163,16 +163,17 @@ class _MobileSpaceTabState extends State<MobileSpaceTab>
|
||||
case MobileSpaceTabType.recent:
|
||||
return const MobileRecentSpace();
|
||||
case MobileSpaceTabType.spaces:
|
||||
final showAIFloatingButton =
|
||||
widget.userProfile.workspaceType == WorkspaceTypePB.ServerW;
|
||||
return Stack(
|
||||
children: [
|
||||
MobileHomeSpace(userProfile: widget.userProfile),
|
||||
if (showAIFloatingButton)
|
||||
// only show ai chat button for cloud user
|
||||
if (widget.userProfile.authenticator ==
|
||||
AuthenticatorPB.AppFlowyCloud)
|
||||
Positioned(
|
||||
right: 20,
|
||||
bottom: MediaQuery.of(context).padding.bottom + 16,
|
||||
child: FloatingAIEntryV2(),
|
||||
left: 20,
|
||||
right: 20,
|
||||
child: const FloatingAIEntry(),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
-1
@@ -123,7 +123,6 @@ class _CreateWorkspaceButton extends StatelessWidget {
|
||||
context.read<UserWorkspaceBloc>().add(
|
||||
UserWorkspaceEvent.createWorkspace(
|
||||
name,
|
||||
WorkspaceTypePB.ServerW,
|
||||
),
|
||||
);
|
||||
},
|
||||
|
||||
+45
-57
@@ -6,10 +6,10 @@ import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/mobile/presentation/notifications/mobile_notifications_screen.dart';
|
||||
import 'package:appflowy/mobile/presentation/widgets/navigation_bar_button.dart';
|
||||
import 'package:appflowy/shared/popup_menu/appflowy_popup_menu.dart';
|
||||
import 'package:appflowy/shared/red_dot.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/user/application/reminder/reminder_bloc.dart';
|
||||
import 'package:appflowy/util/theme_extension.dart';
|
||||
import 'package:appflowy/workspace/presentation/notifications/number_red_dot.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
@@ -28,52 +28,46 @@ final PropertyValueNotifier<ViewLayoutPB?> mobileCreateNewPageNotifier =
|
||||
PropertyValueNotifier(null);
|
||||
final ValueNotifier<BottomNavigationBarActionType> bottomNavigationBarType =
|
||||
ValueNotifier(BottomNavigationBarActionType.home);
|
||||
final ValueNotifier<String?> bottomNavigationBarItemType =
|
||||
ValueNotifier(BottomNavigationBarItemType.home.label);
|
||||
|
||||
enum BottomNavigationBarItemType {
|
||||
home,
|
||||
search,
|
||||
add,
|
||||
notification;
|
||||
|
||||
String get label => name;
|
||||
String get label {
|
||||
return switch (this) {
|
||||
BottomNavigationBarItemType.home => 'home',
|
||||
BottomNavigationBarItemType.add => 'add',
|
||||
BottomNavigationBarItemType.notification => 'notification',
|
||||
};
|
||||
}
|
||||
|
||||
ValueKey get valueKey {
|
||||
return ValueKey(label);
|
||||
}
|
||||
|
||||
Widget get iconWidget {
|
||||
return switch (this) {
|
||||
home => const FlowySvg(FlowySvgs.m_home_unselected_m),
|
||||
search => const FlowySvg(FlowySvgs.m_home_search_icon_m),
|
||||
add => const FlowySvg(FlowySvgs.m_home_add_m),
|
||||
notification => const _NotificationNavigationBarItemIcon(),
|
||||
};
|
||||
}
|
||||
|
||||
Widget? get activeIcon {
|
||||
return switch (this) {
|
||||
home => const FlowySvg(FlowySvgs.m_home_selected_m, blendMode: null),
|
||||
search =>
|
||||
const FlowySvg(FlowySvgs.m_home_search_icon_active_m, blendMode: null),
|
||||
add => null,
|
||||
notification => const _NotificationNavigationBarItemIcon(isActive: true),
|
||||
};
|
||||
}
|
||||
|
||||
BottomNavigationBarItem get navigationItem {
|
||||
return BottomNavigationBarItem(
|
||||
key: valueKey,
|
||||
label: label,
|
||||
icon: iconWidget,
|
||||
activeIcon: activeIcon,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final _items =
|
||||
BottomNavigationBarItemType.values.map((e) => e.navigationItem).toList();
|
||||
final _items = <BottomNavigationBarItem>[
|
||||
BottomNavigationBarItem(
|
||||
key: BottomNavigationBarItemType.home.valueKey,
|
||||
label: BottomNavigationBarItemType.home.label,
|
||||
icon: const FlowySvg(FlowySvgs.m_home_unselected_m),
|
||||
activeIcon: const FlowySvg(FlowySvgs.m_home_selected_m, blendMode: null),
|
||||
),
|
||||
BottomNavigationBarItem(
|
||||
key: BottomNavigationBarItemType.add.valueKey,
|
||||
label: BottomNavigationBarItemType.add.label,
|
||||
icon: const FlowySvg(FlowySvgs.m_home_add_m),
|
||||
),
|
||||
BottomNavigationBarItem(
|
||||
key: BottomNavigationBarItemType.notification.valueKey,
|
||||
label: BottomNavigationBarItemType.notification.label,
|
||||
icon: const _NotificationNavigationBarItemIcon(),
|
||||
activeIcon: const _NotificationNavigationBarItemIcon(
|
||||
isActive: true,
|
||||
),
|
||||
),
|
||||
];
|
||||
|
||||
/// Builds the "shell" for the app by building a Scaffold with a
|
||||
/// BottomNavigationBar, where [child] is placed in the body of the Scaffold.
|
||||
@@ -175,28 +169,23 @@ class _NotificationNavigationBarItemIcon extends StatelessWidget {
|
||||
final hasUnreads = state.reminders.any(
|
||||
(reminder) => !reminder.isRead,
|
||||
);
|
||||
return SizedBox(
|
||||
width: 40,
|
||||
height: 40,
|
||||
child: Stack(
|
||||
children: [
|
||||
Center(
|
||||
child: isActive
|
||||
? const FlowySvg(
|
||||
FlowySvgs.m_home_active_notification_m,
|
||||
blendMode: null,
|
||||
)
|
||||
: const FlowySvg(
|
||||
FlowySvgs.m_home_notification_m,
|
||||
),
|
||||
return Stack(
|
||||
children: [
|
||||
isActive
|
||||
? const FlowySvg(
|
||||
FlowySvgs.m_home_active_notification_m,
|
||||
blendMode: null,
|
||||
)
|
||||
: const FlowySvg(
|
||||
FlowySvgs.m_home_notification_m,
|
||||
),
|
||||
if (hasUnreads)
|
||||
const Positioned(
|
||||
top: 2,
|
||||
right: 4,
|
||||
child: NotificationRedDot(),
|
||||
),
|
||||
if (hasUnreads)
|
||||
const Align(
|
||||
alignment: Alignment.topRight,
|
||||
child: NumberedRedDot.mobile(),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
@@ -270,7 +259,6 @@ class _HomePageNavigationBar extends StatelessWidget {
|
||||
} else if (label == BottomNavigationBarItemType.notification.label) {
|
||||
getIt<ReminderBloc>().add(const ReminderEvent.refresh());
|
||||
}
|
||||
bottomNavigationBarItemType.value = label;
|
||||
// When navigating to a new branch, it's recommended to use the goBranch
|
||||
// method, as doing so makes sure the last navigation state of the
|
||||
// Navigator for the branch is restored.
|
||||
|
||||
+6
-5
@@ -50,9 +50,9 @@ class _MobileNotificationsScreenState extends State<MobileNotificationsScreen>
|
||||
orElse: () =>
|
||||
const Center(child: CircularProgressIndicator.adaptive()),
|
||||
workspaceFailure: () => const WorkspaceFailedScreen(),
|
||||
success: (workspaceLatest, userProfile) =>
|
||||
success: (workspaceSetting, userProfile) =>
|
||||
_NotificationScreenContent(
|
||||
workspaceLatest: workspaceLatest,
|
||||
workspaceSetting: workspaceSetting,
|
||||
userProfile: userProfile,
|
||||
controller: controller,
|
||||
reminderBloc: reminderBloc,
|
||||
@@ -66,13 +66,13 @@ class _MobileNotificationsScreenState extends State<MobileNotificationsScreen>
|
||||
|
||||
class _NotificationScreenContent extends StatelessWidget {
|
||||
const _NotificationScreenContent({
|
||||
required this.workspaceLatest,
|
||||
required this.workspaceSetting,
|
||||
required this.userProfile,
|
||||
required this.controller,
|
||||
required this.reminderBloc,
|
||||
});
|
||||
|
||||
final WorkspaceLatestPB workspaceLatest;
|
||||
final WorkspaceSettingPB workspaceSetting;
|
||||
final UserProfilePB userProfile;
|
||||
final TabController controller;
|
||||
final ReminderBloc reminderBloc;
|
||||
@@ -84,7 +84,7 @@ class _NotificationScreenContent extends StatelessWidget {
|
||||
..add(
|
||||
SidebarSectionsEvent.initial(
|
||||
userProfile,
|
||||
workspaceLatest.workspaceId,
|
||||
workspaceSetting.workspaceId,
|
||||
),
|
||||
),
|
||||
child: BlocBuilder<SidebarSectionsBloc, SidebarSectionsState>(
|
||||
@@ -126,6 +126,7 @@ class _NotificationScreenContent extends StatelessWidget {
|
||||
onAction: _onAction,
|
||||
onReadChanged: _onReadChanged,
|
||||
actionBar: InboxActionBar(
|
||||
hasUnreads: state.hasUnreads,
|
||||
showUnreadsOnly: filterState.showUnreadsOnly,
|
||||
),
|
||||
),
|
||||
|
||||
+3
-6
@@ -3,14 +3,11 @@ import 'package:appflowy/mobile/presentation/notifications/widgets/widgets.dart'
|
||||
import 'package:appflowy/mobile/presentation/presentation.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/user/application/reminder/reminder_bloc.dart';
|
||||
import 'package:appflowy/workspace/presentation/notifications/widgets/notification_tab_bar.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import 'widgets/tab_bar.dart';
|
||||
|
||||
final PropertyValueNotifier<List<String>> mSelectedNotificationIds =
|
||||
PropertyValueNotifier([]);
|
||||
|
||||
@@ -79,9 +76,9 @@ class _MobileNotificationsTabState extends State<MobileNotificationsTab>
|
||||
late TabController tabController;
|
||||
|
||||
final tabs = [
|
||||
NotificationTabType.inbox,
|
||||
NotificationTabType.unread,
|
||||
NotificationTabType.archive,
|
||||
MobileNotificationTabType.inbox,
|
||||
MobileNotificationTabType.unread,
|
||||
MobileNotificationTabType.archive,
|
||||
];
|
||||
|
||||
@override
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/workspace/presentation/notifications/widgets/notification_tab_bar.dart';
|
||||
import 'package:appflowy/mobile/presentation/notifications/widgets/widgets.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -11,24 +11,24 @@ class EmptyNotification extends StatelessWidget {
|
||||
required this.type,
|
||||
});
|
||||
|
||||
final NotificationTabType type;
|
||||
final MobileNotificationTabType type;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final title = switch (type) {
|
||||
NotificationTabType.inbox =>
|
||||
MobileNotificationTabType.inbox =>
|
||||
LocaleKeys.settings_notifications_emptyInbox_title.tr(),
|
||||
NotificationTabType.archive =>
|
||||
MobileNotificationTabType.archive =>
|
||||
LocaleKeys.settings_notifications_emptyArchived_title.tr(),
|
||||
NotificationTabType.unread =>
|
||||
MobileNotificationTabType.unread =>
|
||||
LocaleKeys.settings_notifications_emptyUnread_title.tr(),
|
||||
};
|
||||
final desc = switch (type) {
|
||||
NotificationTabType.inbox =>
|
||||
MobileNotificationTabType.inbox =>
|
||||
LocaleKeys.settings_notifications_emptyInbox_description.tr(),
|
||||
NotificationTabType.archive =>
|
||||
MobileNotificationTabType.archive =>
|
||||
LocaleKeys.settings_notifications_emptyArchived_description.tr(),
|
||||
NotificationTabType.unread =>
|
||||
MobileNotificationTabType.unread =>
|
||||
LocaleKeys.settings_notifications_emptyUnread_description.tr(),
|
||||
};
|
||||
return Column(
|
||||
|
||||
+3
-1
@@ -101,7 +101,9 @@ class _InnerNotificationItem extends StatelessWidget {
|
||||
NotificationCheckIcon(
|
||||
isSelected: mSelectedNotificationIds.value.contains(reminder.id),
|
||||
),
|
||||
const HSpace(12.0),
|
||||
const HSpace(3.0),
|
||||
!reminder.isRead ? const UnreadRedDot() : const HSpace(6.0),
|
||||
const HSpace(3.0),
|
||||
NotificationIcon(reminder: reminder),
|
||||
const HSpace(12.0),
|
||||
Expanded(
|
||||
|
||||
+18
-21
@@ -4,7 +4,6 @@ import 'package:appflowy/mobile/presentation/base/animated_gesture.dart';
|
||||
import 'package:appflowy/mobile/presentation/notifications/widgets/widgets.dart';
|
||||
import 'package:appflowy/user/application/reminder/reminder_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
|
||||
import 'package:appflowy/workspace/presentation/notifications/widgets/notification_tab_bar.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
@@ -19,7 +18,7 @@ class NotificationItem extends StatelessWidget {
|
||||
required this.reminder,
|
||||
});
|
||||
|
||||
final NotificationTabType tabType;
|
||||
final MobileNotificationTabType tabType;
|
||||
final ReminderPB reminder;
|
||||
|
||||
@override
|
||||
@@ -48,19 +47,14 @@ class NotificationItem extends StatelessWidget {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
final child = GestureDetector(
|
||||
onLongPress: () {
|
||||
context.onMoreAction();
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: _SlidableNotificationItem(
|
||||
final child = Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: _SlidableNotificationItem(
|
||||
tabType: tabType,
|
||||
reminder: reminder,
|
||||
child: _InnerNotificationItem(
|
||||
tabType: tabType,
|
||||
reminder: reminder,
|
||||
child: _InnerNotificationItem(
|
||||
tabType: tabType,
|
||||
reminder: reminder,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -99,7 +93,7 @@ class _InnerNotificationItem extends StatelessWidget {
|
||||
required this.tabType,
|
||||
});
|
||||
|
||||
final NotificationTabType tabType;
|
||||
final MobileNotificationTabType tabType;
|
||||
final ReminderPB reminder;
|
||||
|
||||
@override
|
||||
@@ -107,11 +101,14 @@ class _InnerNotificationItem extends StatelessWidget {
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const HSpace(10.0),
|
||||
const HSpace(8.0),
|
||||
!reminder.isRead ? const UnreadRedDot() : const HSpace(6.0),
|
||||
const HSpace(4.0),
|
||||
NotificationIcon(reminder: reminder),
|
||||
const HSpace(12.0),
|
||||
Expanded(child: NotificationContent(reminder: reminder)),
|
||||
const HSpace(6.0),
|
||||
Expanded(
|
||||
child: NotificationContent(reminder: reminder),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -124,22 +121,22 @@ class _SlidableNotificationItem extends StatelessWidget {
|
||||
required this.child,
|
||||
});
|
||||
|
||||
final NotificationTabType tabType;
|
||||
final MobileNotificationTabType tabType;
|
||||
final ReminderPB reminder;
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final List<NotificationPaneActionType> actions = switch (tabType) {
|
||||
NotificationTabType.inbox => [
|
||||
MobileNotificationTabType.inbox => [
|
||||
NotificationPaneActionType.more,
|
||||
if (!reminder.isRead) NotificationPaneActionType.markAsRead,
|
||||
],
|
||||
NotificationTabType.unread => [
|
||||
MobileNotificationTabType.unread => [
|
||||
NotificationPaneActionType.more,
|
||||
NotificationPaneActionType.markAsRead,
|
||||
],
|
||||
NotificationTabType.archive => [
|
||||
MobileNotificationTabType.archive => [
|
||||
if (kDebugMode) NotificationPaneActionType.unArchive,
|
||||
],
|
||||
};
|
||||
|
||||
+48
-101
@@ -10,11 +10,11 @@ import 'package:appflowy/workspace/application/view/view_ext.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:appflowy_ui/appflowy_ui.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';
|
||||
import 'package:universal_platform/universal_platform.dart';
|
||||
|
||||
const _kNotificationIconHeight = 36.0;
|
||||
|
||||
@@ -22,45 +22,16 @@ class NotificationIcon extends StatelessWidget {
|
||||
const NotificationIcon({
|
||||
super.key,
|
||||
required this.reminder,
|
||||
this.atSize = 12,
|
||||
});
|
||||
|
||||
final ReminderPB reminder;
|
||||
final double atSize;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = AppFlowyTheme.of(context);
|
||||
return SizedBox(
|
||||
width: 42,
|
||||
height: 36,
|
||||
child: Stack(
|
||||
children: [
|
||||
const FlowySvg(
|
||||
FlowySvgs.m_notification_reminder_s,
|
||||
size: Size.square(32),
|
||||
blendMode: null,
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment.bottomRight,
|
||||
child: Container(
|
||||
width: 20,
|
||||
height: 20,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: theme.fillColorScheme.primary,
|
||||
),
|
||||
child: Center(
|
||||
child: FlowySvg(
|
||||
FlowySvgs.notification_icon_at_s,
|
||||
size: Size.square(atSize),
|
||||
color: theme.iconColorScheme.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
return const FlowySvg(
|
||||
FlowySvgs.m_notification_reminder_s,
|
||||
size: Size.square(_kNotificationIconHeight),
|
||||
blendMode: null,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -91,15 +62,14 @@ class UnreadRedDot extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = AppFlowyTheme.of(context);
|
||||
return SizedBox(
|
||||
return const SizedBox(
|
||||
height: _kNotificationIconHeight,
|
||||
child: Center(
|
||||
child: SizedBox.square(
|
||||
dimension: 7.0,
|
||||
dimension: 6.0,
|
||||
child: DecoratedBox(
|
||||
decoration: ShapeDecoration(
|
||||
color: theme.borderColorScheme.errorThick,
|
||||
color: Color(0xFFFF6331),
|
||||
shape: OvalBorder(),
|
||||
),
|
||||
),
|
||||
@@ -122,8 +92,6 @@ class NotificationContent extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _NotificationContentState extends State<NotificationContent> {
|
||||
AppFlowyThemeData get theme => AppFlowyTheme.of(context);
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant NotificationContent oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
@@ -146,12 +114,21 @@ class _NotificationContentState extends State<NotificationContent> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// title & time
|
||||
_buildHeader(state.scheduledAt, !widget.reminder.isRead),
|
||||
// page name
|
||||
_buildPageName(context, state.isLocked, state.pageTitle),
|
||||
// title
|
||||
_buildHeader(),
|
||||
|
||||
// time & page name
|
||||
_buildTimeAndPageName(
|
||||
context,
|
||||
state.createdAt,
|
||||
state.pageTitle,
|
||||
),
|
||||
|
||||
// content
|
||||
_buildContent(view, nodes: state.nodes),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 16.0),
|
||||
child: _buildContent(view, nodes: state.nodes),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
@@ -178,8 +155,6 @@ class _NotificationContentState extends State<NotificationContent> {
|
||||
fontSize: 14,
|
||||
figmaLineHeight: 22,
|
||||
color: context.notificationItemTextColor,
|
||||
maxLines: 3,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -187,72 +162,40 @@ class _NotificationContentState extends State<NotificationContent> {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
Widget _buildHeader(String createAt, bool unread) {
|
||||
return SizedBox(
|
||||
height: 22,
|
||||
child: Row(
|
||||
children: [
|
||||
FlowyText.semibold(
|
||||
LocaleKeys.settings_notifications_titles_reminder.tr(),
|
||||
fontSize: 14,
|
||||
figmaLineHeight: 20,
|
||||
color: theme.textColorScheme.primary,
|
||||
),
|
||||
Spacer(),
|
||||
if (createAt.isNotEmpty)
|
||||
FlowyText.regular(
|
||||
createAt,
|
||||
fontSize: 12,
|
||||
figmaLineHeight: 18,
|
||||
color: theme.textColorScheme.secondary,
|
||||
),
|
||||
if (unread) ...[
|
||||
HSpace(4),
|
||||
const UnreadRedDot(),
|
||||
],
|
||||
],
|
||||
),
|
||||
Widget _buildHeader() {
|
||||
return FlowyText.semibold(
|
||||
LocaleKeys.settings_notifications_titles_reminder.tr(),
|
||||
fontSize: 14,
|
||||
figmaLineHeight: 20,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPageName(
|
||||
Widget _buildTimeAndPageName(
|
||||
BuildContext context,
|
||||
bool isLocked,
|
||||
String createdAt,
|
||||
String pageTitle,
|
||||
) {
|
||||
return Opacity(
|
||||
opacity: 0.5,
|
||||
child: SizedBox(
|
||||
height: 18,
|
||||
child: Row(
|
||||
children: [
|
||||
/// TODO: need to be replaced after reminder support more types
|
||||
child: Row(
|
||||
children: [
|
||||
// the legacy reminder doesn't contain the timestamp, so we don't show it
|
||||
if (createdAt.isNotEmpty) ...[
|
||||
FlowyText.regular(
|
||||
LocaleKeys.notificationHub_mentionedYou.tr(),
|
||||
createdAt,
|
||||
fontSize: 12,
|
||||
figmaLineHeight: 18,
|
||||
color: theme.textColorScheme.secondary,
|
||||
color: context.notificationItemTextColor,
|
||||
),
|
||||
const NotificationEllipse(),
|
||||
if (isLocked)
|
||||
Padding(
|
||||
padding: EdgeInsets.only(right: 5),
|
||||
child: FlowySvg(
|
||||
FlowySvgs.notification_lock_s,
|
||||
color: theme.iconColorScheme.secondary,
|
||||
),
|
||||
),
|
||||
Flexible(
|
||||
child: FlowyText.regular(
|
||||
pageTitle,
|
||||
fontSize: 12,
|
||||
figmaLineHeight: 18,
|
||||
color: theme.textColorScheme.secondary,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
FlowyText.regular(
|
||||
pageTitle,
|
||||
fontSize: 12,
|
||||
figmaLineHeight: 18,
|
||||
color: context.notificationItemTextColor,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -266,7 +209,7 @@ class NotificationEllipse extends StatelessWidget {
|
||||
return Container(
|
||||
width: 2.50,
|
||||
height: 2.50,
|
||||
margin: const EdgeInsets.symmetric(horizontal: 6.0),
|
||||
margin: const EdgeInsets.symmetric(horizontal: 5.0),
|
||||
decoration: ShapeDecoration(
|
||||
color: context.notificationItemTextColor,
|
||||
shape: const OvalBorder(),
|
||||
@@ -322,7 +265,11 @@ class NotificationDocumentContent extends StatelessWidget {
|
||||
styleCustomizer: styleCustomizer,
|
||||
// the editor is not editable in the chat
|
||||
editable: false,
|
||||
customPadding: (node) => EdgeInsets.zero,
|
||||
customHeadingPadding: UniversalPlatform.isDesktop
|
||||
? EdgeInsets.zero
|
||||
: EdgeInsets.symmetric(
|
||||
vertical: EditorStyleCustomizer.nodeHorizontalPadding,
|
||||
),
|
||||
);
|
||||
|
||||
return IgnorePointer(
|
||||
|
||||
+32
-34
@@ -2,12 +2,12 @@ import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/mobile/application/notification/notification_reminder_bloc.dart';
|
||||
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
|
||||
import 'package:appflowy/mobile/presentation/notifications/widgets/widgets.dart';
|
||||
import 'package:appflowy/mobile/presentation/page_item/mobile_slide_action_button.dart';
|
||||
import 'package:appflowy/mobile/presentation/presentation.dart';
|
||||
import 'package:appflowy/mobile/presentation/widgets/widgets.dart';
|
||||
import 'package:appflowy/user/application/reminder/reminder_bloc.dart';
|
||||
import 'package:appflowy/user/application/reminder/reminder_extension.dart';
|
||||
import 'package:appflowy/workspace/presentation/notifications/widgets/notification_tab_bar.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -21,7 +21,7 @@ enum NotificationPaneActionType {
|
||||
|
||||
MobileSlideActionButton actionButton(
|
||||
BuildContext context, {
|
||||
required NotificationTabType tabType,
|
||||
required MobileNotificationTabType tabType,
|
||||
}) {
|
||||
switch (this) {
|
||||
case NotificationPaneActionType.markAsRead:
|
||||
@@ -76,43 +76,41 @@ enum NotificationPaneActionType {
|
||||
topLeft: Radius.circular(10),
|
||||
bottomLeft: Radius.circular(10),
|
||||
),
|
||||
onPressed: (context) => context.onMoreAction(),
|
||||
onPressed: (context) {
|
||||
final reminderBloc = context.read<ReminderBloc>();
|
||||
final notificationReminderBloc =
|
||||
context.read<NotificationReminderBloc>();
|
||||
|
||||
showMobileBottomSheet(
|
||||
context,
|
||||
showDragHandle: true,
|
||||
showDivider: false,
|
||||
useRootNavigator: true,
|
||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||
builder: (_) {
|
||||
return MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider.value(value: reminderBloc),
|
||||
BlocProvider.value(value: notificationReminderBloc),
|
||||
],
|
||||
child: _NotificationMoreActions(
|
||||
onClickMultipleChoice: () {
|
||||
Future.delayed(const Duration(milliseconds: 250), () {
|
||||
bottomNavigationBarType.value =
|
||||
BottomNavigationBarActionType
|
||||
.notificationMultiSelect;
|
||||
});
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension NotificationPaneActionExtension on BuildContext {
|
||||
void onMoreAction() {
|
||||
final reminderBloc = read<ReminderBloc>();
|
||||
final notificationReminderBloc = read<NotificationReminderBloc>();
|
||||
|
||||
showMobileBottomSheet(
|
||||
this,
|
||||
showDragHandle: true,
|
||||
showDivider: false,
|
||||
useRootNavigator: true,
|
||||
backgroundColor: Theme.of(this).colorScheme.surface,
|
||||
builder: (_) {
|
||||
return MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider.value(value: reminderBloc),
|
||||
BlocProvider.value(value: notificationReminderBloc),
|
||||
],
|
||||
child: _NotificationMoreActions(
|
||||
onClickMultipleChoice: () {
|
||||
Future.delayed(const Duration(milliseconds: 250), () {
|
||||
bottomNavigationBarType.value =
|
||||
BottomNavigationBarActionType.notificationMultiSelect;
|
||||
});
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _NotificationMoreActions extends StatelessWidget {
|
||||
const _NotificationMoreActions({
|
||||
required this.onClickMultipleChoice,
|
||||
|
||||
@@ -3,7 +3,6 @@ import 'package:appflowy/mobile/presentation/notifications/widgets/widgets.dart'
|
||||
import 'package:appflowy/shared/list_extension.dart';
|
||||
import 'package:appflowy/user/application/reminder/reminder_bloc.dart';
|
||||
import 'package:appflowy/user/application/reminder/reminder_extension.dart';
|
||||
import 'package:appflowy/workspace/presentation/notifications/widgets/notification_tab_bar.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
||||
import 'package:appflowy_backend/appflowy_backend.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
|
||||
@@ -18,7 +17,7 @@ class NotificationTab extends StatefulWidget {
|
||||
required this.tabType,
|
||||
});
|
||||
|
||||
final NotificationTabType tabType;
|
||||
final MobileNotificationTabType tabType;
|
||||
|
||||
@override
|
||||
State<NotificationTab> createState() => _NotificationTabState();
|
||||
@@ -82,17 +81,17 @@ class _NotificationTabState extends State<NotificationTab>
|
||||
|
||||
List<ReminderPB> _filterReminders(List<ReminderPB> reminders) {
|
||||
switch (widget.tabType) {
|
||||
case NotificationTabType.inbox:
|
||||
case MobileNotificationTabType.inbox:
|
||||
return reminders.reversed
|
||||
.where((reminder) => !reminder.isArchived)
|
||||
.toList()
|
||||
.unique((reminder) => reminder.id);
|
||||
case NotificationTabType.archive:
|
||||
case MobileNotificationTabType.archive:
|
||||
return reminders.reversed
|
||||
.where((reminder) => reminder.isArchived)
|
||||
.toList()
|
||||
.unique((reminder) => reminder.id);
|
||||
case NotificationTabType.unread:
|
||||
case MobileNotificationTabType.unread:
|
||||
return reminders.reversed
|
||||
.where((reminder) => !reminder.isRead)
|
||||
.toList()
|
||||
|
||||
+34
-2
@@ -1,8 +1,40 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/mobile/presentation/home/tab/_round_underline_tab_indicator.dart';
|
||||
import 'package:appflowy/workspace/presentation/notifications/widgets/notification_tab_bar.dart';
|
||||
import 'package:appflowy/mobile/presentation/notifications/widgets/widgets.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:reorderable_tabbar/reorderable_tabbar.dart';
|
||||
|
||||
enum MobileNotificationTabType {
|
||||
inbox,
|
||||
unread,
|
||||
archive;
|
||||
|
||||
String get tr {
|
||||
switch (this) {
|
||||
case MobileNotificationTabType.inbox:
|
||||
return LocaleKeys.settings_notifications_tabs_inbox.tr();
|
||||
case MobileNotificationTabType.unread:
|
||||
return LocaleKeys.settings_notifications_tabs_unread.tr();
|
||||
case MobileNotificationTabType.archive:
|
||||
return LocaleKeys.settings_notifications_tabs_archived.tr();
|
||||
}
|
||||
}
|
||||
|
||||
List<NotificationPaneActionType> get actions {
|
||||
switch (this) {
|
||||
case MobileNotificationTabType.inbox:
|
||||
return [
|
||||
NotificationPaneActionType.more,
|
||||
NotificationPaneActionType.markAsRead,
|
||||
];
|
||||
case MobileNotificationTabType.unread:
|
||||
case MobileNotificationTabType.archive:
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class MobileNotificationTabBar extends StatelessWidget {
|
||||
const MobileNotificationTabBar({
|
||||
super.key,
|
||||
@@ -12,7 +44,7 @@ class MobileNotificationTabBar extends StatelessWidget {
|
||||
});
|
||||
|
||||
final double height;
|
||||
final List<NotificationTabType> tabs;
|
||||
final List<MobileNotificationTabType> tabs;
|
||||
final TabController tabController;
|
||||
|
||||
@override
|
||||
|
||||
@@ -6,3 +6,4 @@ export 'settings_popup_menu.dart';
|
||||
export 'shared.dart';
|
||||
export 'slide_actions.dart';
|
||||
export 'tab.dart';
|
||||
export 'tab_bar.dart';
|
||||
-76
@@ -1,76 +0,0 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/mobile/presentation/home/tab/mobile_space_tab.dart';
|
||||
import 'package:appflowy/workspace/application/command_palette/command_palette_bloc.dart';
|
||||
import 'package:appflowy_ui/appflowy_ui.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 MobileSearchAskAiEntrance extends StatelessWidget {
|
||||
const MobileSearchAskAiEntrance({super.key, this.query});
|
||||
final String? query;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = AppFlowyTheme.of(context);
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
context
|
||||
.read<CommandPaletteBloc?>()
|
||||
?.add(CommandPaletteEvent.gointToAskAI());
|
||||
mobileCreateNewAIChatNotifier.value =
|
||||
mobileCreateNewAIChatNotifier.value + 1;
|
||||
},
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: Container(
|
||||
height: 48,
|
||||
margin: EdgeInsets.only(top: 16),
|
||||
padding: EdgeInsets.fromLTRB(8, 12, 8, 12),
|
||||
child: Row(
|
||||
children: [
|
||||
FlowySvg(
|
||||
FlowySvgs.m_home_ai_chat_icon_m,
|
||||
size: Size.square(24),
|
||||
blendMode: null,
|
||||
),
|
||||
HSpace(12),
|
||||
buildText(theme),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildText(AppFlowyThemeData theme) {
|
||||
final queryText = query ?? '';
|
||||
if (queryText.isEmpty) {
|
||||
return Text(
|
||||
LocaleKeys.search_askAIAnything.tr(),
|
||||
style: theme.textStyle.heading4
|
||||
.standard(color: theme.textColorScheme.primary),
|
||||
);
|
||||
}
|
||||
return Flexible(
|
||||
child: RichText(
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
text: TextSpan(
|
||||
children: [
|
||||
TextSpan(
|
||||
text: LocaleKeys.search_askAIFor.tr(),
|
||||
style: theme.textStyle.heading4
|
||||
.standard(color: theme.textColorScheme.primary),
|
||||
),
|
||||
TextSpan(
|
||||
text: ' "$queryText"',
|
||||
style: theme.textStyle.heading4
|
||||
.enhanced(color: theme.textColorScheme.primary),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,173 +0,0 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/workspace/application/command_palette/command_palette_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/command_palette/search_result_ext.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_ext.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-search/result.pb.dart';
|
||||
import 'package:appflowy_ui/appflowy_ui.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/widget/spacing.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import 'mobile_view_ancestors.dart';
|
||||
|
||||
class MobileSearchResultCell extends StatelessWidget {
|
||||
const MobileSearchResultCell({super.key, required this.item, this.query});
|
||||
final SearchResultItem item;
|
||||
final String? query;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = AppFlowyTheme.of(context),
|
||||
textColor = theme.textColorScheme.primary;
|
||||
final commandPaletteState = context.read<CommandPaletteBloc>().state;
|
||||
final displayName = item.displayName.isEmpty
|
||||
? LocaleKeys.menuAppHeader_defaultNewPageName.tr()
|
||||
: item.displayName;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 12),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox.square(
|
||||
dimension: 24,
|
||||
child: Center(child: buildIcon(theme)),
|
||||
),
|
||||
HSpace(12),
|
||||
Flexible(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
RichText(
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
text: buildHighLightSpan(
|
||||
content: displayName,
|
||||
normal: theme.textStyle.heading4.standard(color: textColor),
|
||||
highlight: theme.textStyle.heading4
|
||||
.standard(color: textColor)
|
||||
.copyWith(
|
||||
backgroundColor: theme.fillColorScheme.themeSelect,
|
||||
),
|
||||
),
|
||||
),
|
||||
buildPath(commandPaletteState, theme),
|
||||
...buildSummary(theme),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildIcon(AppFlowyThemeData theme) {
|
||||
final icon = item.icon;
|
||||
if (icon.ty == ResultIconTypePB.Emoji) {
|
||||
return icon.getIcon(size: 20) ?? SizedBox.shrink();
|
||||
} else {
|
||||
return icon.getIcon(size: 20, iconColor: theme.iconColorScheme.primary) ??
|
||||
SizedBox.shrink();
|
||||
}
|
||||
}
|
||||
|
||||
Widget buildPath(CommandPaletteState state, AppFlowyThemeData theme) {
|
||||
return BlocProvider(
|
||||
create: (context) => ViewAncestorBloc(item.id),
|
||||
child: BlocBuilder<ViewAncestorBloc, ViewAncestorState>(
|
||||
builder: (context, state) {
|
||||
final ancestors = state.ancestor.ancestors;
|
||||
List<String> displayPath = ancestors.map((e) => e.name).toList();
|
||||
if (ancestors.length > 2) {
|
||||
displayPath = [ancestors.first.name, '...', ancestors.last.name];
|
||||
}
|
||||
return Text(
|
||||
displayPath.join(' / '),
|
||||
style: theme.textStyle.body
|
||||
.standard(color: theme.textColorScheme.secondary),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> buildSummary(AppFlowyThemeData theme) {
|
||||
if (item.content.isEmpty) return [];
|
||||
return [
|
||||
VSpace(theme.spacing.m),
|
||||
RichText(
|
||||
maxLines: 3,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
text: buildHighLightSpan(
|
||||
content: item.content,
|
||||
normal: theme.textStyle.heading4
|
||||
.standard(color: theme.textColorScheme.secondary),
|
||||
highlight: theme.textStyle.heading4
|
||||
.standard(color: theme.textColorScheme.primary)
|
||||
.copyWith(
|
||||
backgroundColor: theme.fillColorScheme.themeSelect,
|
||||
),
|
||||
),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
TextSpan buildHighLightSpan({
|
||||
required String content,
|
||||
required TextStyle normal,
|
||||
required TextStyle highlight,
|
||||
}) {
|
||||
final queryText = (query ?? '').trim();
|
||||
if (queryText.isEmpty) {
|
||||
return TextSpan(text: content, style: normal);
|
||||
}
|
||||
final contents = content.splitIncludeSeparator(queryText);
|
||||
return TextSpan(
|
||||
children: List.generate(contents.length, (index) {
|
||||
final content = contents[index];
|
||||
final isHighlight = content.toLowerCase() == queryText.toLowerCase();
|
||||
return TextSpan(
|
||||
text: content,
|
||||
style: isHighlight ? highlight : normal,
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
extension ViewPBToSearchResultItem on ViewPB {
|
||||
SearchResultItem toSearchResultItem() {
|
||||
final hasIcon = icon.value.isNotEmpty;
|
||||
return SearchResultItem(
|
||||
id: id,
|
||||
displayName: nameOrDefault,
|
||||
icon: ResultIconPB(
|
||||
ty: hasIcon
|
||||
? ResultIconTypePB.valueOf(icon.ty.value)
|
||||
: ResultIconTypePB.Icon,
|
||||
value: hasIcon ? icon.value : '${layout.value}',
|
||||
),
|
||||
content: '',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
extension StringSplitExtension on String {
|
||||
List<String> splitIncludeSeparator(String separator) {
|
||||
final splits =
|
||||
split(RegExp(RegExp.escape(separator), caseSensitive: false));
|
||||
final List<String> contents = [];
|
||||
int charIndex = 0;
|
||||
final seperatorLength = separator.length;
|
||||
for (int i = 0; i < splits.length; i++) {
|
||||
contents.add(splits[i]);
|
||||
charIndex += splits[i].length;
|
||||
if (i != splits.length - 1) {
|
||||
contents.add(substring(charIndex, charIndex + seperatorLength));
|
||||
charIndex += seperatorLength;
|
||||
}
|
||||
}
|
||||
return contents;
|
||||
}
|
||||
}
|
||||
@@ -1,139 +0,0 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/user/application/auth/auth_service.dart';
|
||||
import 'package:appflowy/workspace/application/command_palette/command_palette_bloc.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/errors/workspace_failed_screen.dart';
|
||||
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/workspace.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'mobile_search_ask_ai_entrance.dart';
|
||||
import 'mobile_search_result.dart';
|
||||
import 'mobile_search_textfield.dart';
|
||||
|
||||
class MobileSearchScreen extends StatelessWidget {
|
||||
const MobileSearchScreen({
|
||||
super.key,
|
||||
});
|
||||
|
||||
static const routeName = '/search';
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FutureBuilder(
|
||||
future: Future.wait([
|
||||
FolderEventGetCurrentWorkspaceSetting().send(),
|
||||
getIt<AuthService>().getUser(),
|
||||
]),
|
||||
builder: (context, snapshots) {
|
||||
if (!snapshots.hasData) {
|
||||
return const Center(child: CircularProgressIndicator.adaptive());
|
||||
}
|
||||
|
||||
final latest = snapshots.data?[0].fold(
|
||||
(latest) {
|
||||
return latest as WorkspaceLatestPB?;
|
||||
},
|
||||
(error) => null,
|
||||
);
|
||||
final userProfile = snapshots.data?[1].fold(
|
||||
(userProfilePB) {
|
||||
return userProfilePB as UserProfilePB?;
|
||||
},
|
||||
(error) => null,
|
||||
);
|
||||
|
||||
// In the unlikely case either of the above is null, eg.
|
||||
// when a workspace is already open this can happen.
|
||||
if (latest == null || userProfile == null) {
|
||||
return const WorkspaceFailedScreen();
|
||||
}
|
||||
|
||||
return Provider.value(
|
||||
value: userProfile,
|
||||
child: MobileSearchPage(
|
||||
userProfile: userProfile,
|
||||
workspaceLatestPB: latest,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MobileSearchPage extends StatefulWidget {
|
||||
const MobileSearchPage({
|
||||
super.key,
|
||||
required this.userProfile,
|
||||
required this.workspaceLatestPB,
|
||||
});
|
||||
|
||||
final UserProfilePB userProfile;
|
||||
final WorkspaceLatestPB workspaceLatestPB;
|
||||
|
||||
@override
|
||||
State<MobileSearchPage> createState() => _MobileSearchPageState();
|
||||
}
|
||||
|
||||
class _MobileSearchPageState extends State<MobileSearchPage> {
|
||||
bool get enableShowAISearch =>
|
||||
widget.userProfile.workspaceType == WorkspaceTypePB.ServerW;
|
||||
|
||||
final focusNode = FocusNode();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
focusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<CommandPaletteBloc, CommandPaletteState>(
|
||||
builder: (context, state) {
|
||||
return SafeArea(
|
||||
child: Scaffold(
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MobileSearchTextfield(
|
||||
focusNode: focusNode,
|
||||
hintText: enableShowAISearch
|
||||
? LocaleKeys.search_searchOrAskAI.tr()
|
||||
: LocaleKeys.search_label.tr(),
|
||||
query: state.query ?? '',
|
||||
onChanged: (value) =>
|
||||
context.read<CommandPaletteBloc>().add(
|
||||
CommandPaletteEvent.searchChanged(search: value),
|
||||
),
|
||||
),
|
||||
if (enableShowAISearch)
|
||||
MobileSearchAskAiEntrance(query: state.query),
|
||||
Flexible(
|
||||
child: NotificationListener(
|
||||
child: MobileSearchResult(),
|
||||
onNotification: (t) {
|
||||
if (t is ScrollUpdateNotification) {
|
||||
if (focusNode.hasFocus) {
|
||||
focusNode.unfocus();
|
||||
}
|
||||
}
|
||||
return true;
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,200 +0,0 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/mobile/application/mobile_router.dart';
|
||||
import 'package:appflowy/shared/icon_emoji_picker/tab.dart';
|
||||
import 'package:appflowy/workspace/application/command_palette/command_palette_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/command_palette/search_result_list_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/recent/recent_views_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_service.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:appflowy_result/appflowy_result.dart';
|
||||
import 'package:appflowy_ui/appflowy_ui.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/widget/spacing.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import 'mobile_search_cell.dart';
|
||||
|
||||
class MobileSearchResult extends StatelessWidget {
|
||||
const MobileSearchResult({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final state = context.read<CommandPaletteBloc>().state;
|
||||
final query = (state.query ?? '').trim();
|
||||
if (query.isEmpty) {
|
||||
return const MobileSearchRecentList();
|
||||
}
|
||||
return MobileSearchResultList();
|
||||
}
|
||||
}
|
||||
|
||||
class MobileSearchRecentList extends StatelessWidget {
|
||||
const MobileSearchRecentList({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = AppFlowyTheme.of(context);
|
||||
final commandPaletteState = context.read<CommandPaletteBloc>().state;
|
||||
|
||||
final trashIdSet = commandPaletteState.trash.map((e) => e.id).toSet();
|
||||
return BlocProvider(
|
||||
create: (context) =>
|
||||
RecentViewsBloc()..add(const RecentViewsEvent.initial()),
|
||||
child: BlocBuilder<RecentViewsBloc, RecentViewsState>(
|
||||
builder: (context, state) {
|
||||
final List<ViewPB> recentViews = state.views
|
||||
.map((e) => e.item)
|
||||
.where((e) => !trashIdSet.contains(e.id))
|
||||
.toList();
|
||||
return SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const VSpace(16),
|
||||
Text(
|
||||
LocaleKeys.sideBar_recent.tr(),
|
||||
style: theme.textStyle.body
|
||||
.enhanced(color: theme.textColorScheme.secondary),
|
||||
),
|
||||
const VSpace(4),
|
||||
Column(
|
||||
children: List.generate(recentViews.length, (index) {
|
||||
final view = recentViews[index];
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () => _goToView(context, view),
|
||||
child: MobileSearchResultCell(
|
||||
item: view.toSearchResultItem(),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MobileSearchResultList extends StatefulWidget {
|
||||
const MobileSearchResultList({super.key});
|
||||
|
||||
@override
|
||||
State<MobileSearchResultList> createState() => _MobileSearchResultListState();
|
||||
}
|
||||
|
||||
class _MobileSearchResultListState extends State<MobileSearchResultList> {
|
||||
late final SearchResultListBloc bloc;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
bloc = SearchResultListBloc();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
bloc.close();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final state = context.read<CommandPaletteBloc>().state,
|
||||
theme = AppFlowyTheme.of(context);
|
||||
final isSearching = state.searching,
|
||||
items = state.combinedResponseItems.values.toList(),
|
||||
hasData = items.isNotEmpty;
|
||||
if (isSearching && !hasData) {
|
||||
return Center(child: CircularProgressIndicator.adaptive());
|
||||
} else if (!hasData) {
|
||||
return buildNoResult(state.query ?? '');
|
||||
}
|
||||
return SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const VSpace(16),
|
||||
Text(
|
||||
LocaleKeys.commandPalette_bestMatches.tr(),
|
||||
style: theme.textStyle.body
|
||||
.enhanced(color: theme.textColorScheme.secondary),
|
||||
),
|
||||
const VSpace(4),
|
||||
Column(
|
||||
children: List.generate(items.length, (index) {
|
||||
final item = items[index];
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () async {
|
||||
final view = await ViewBackendService.getView(item.id)
|
||||
.fold((s) => s, (s) => null);
|
||||
if (view != null && context.mounted) {
|
||||
await _goToView(context, view);
|
||||
} else {
|
||||
Log.error(
|
||||
'tapping search result, view not found: ${item.id}',
|
||||
);
|
||||
}
|
||||
},
|
||||
child: MobileSearchResultCell(
|
||||
item: item,
|
||||
query: state.query,
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildNoResult(String query) {
|
||||
final theme = AppFlowyTheme.of(context),
|
||||
textColor = theme.textColorScheme.secondary;
|
||||
return Align(
|
||||
alignment: Alignment.topCenter,
|
||||
child: SizedBox(
|
||||
height: 140,
|
||||
child: Column(
|
||||
children: [
|
||||
const VSpace(48),
|
||||
FlowySvg(
|
||||
FlowySvgs.m_home_search_icon_m,
|
||||
color: theme.iconColorScheme.secondary,
|
||||
size: Size.square(24),
|
||||
),
|
||||
const VSpace(12),
|
||||
Text(
|
||||
LocaleKeys.search_noResultForSearching.tr(),
|
||||
style: theme.textStyle.body.enhanced(color: textColor),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
Text(
|
||||
textAlign: TextAlign.center,
|
||||
LocaleKeys.search_noResultForSearchingHint.tr(),
|
||||
style: theme.textStyle.caption.standard(color: textColor),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _goToView(BuildContext context, ViewPB view) async {
|
||||
await context.pushView(
|
||||
view,
|
||||
tabs: [
|
||||
PickerTabType.emoji,
|
||||
PickerTabType.icon,
|
||||
PickerTabType.custom,
|
||||
].map((e) => e.name).toList(),
|
||||
);
|
||||
}
|
||||
-178
@@ -1,178 +0,0 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/mobile/presentation/mobile_bottom_navigation_bar.dart';
|
||||
import 'package:appflowy_ui/appflowy_ui.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class MobileSearchTextfield extends StatefulWidget {
|
||||
const MobileSearchTextfield({
|
||||
super.key,
|
||||
this.onChanged,
|
||||
required this.hintText,
|
||||
required this.query,
|
||||
required this.focusNode,
|
||||
});
|
||||
|
||||
final String hintText;
|
||||
final String query;
|
||||
final ValueChanged<String>? onChanged;
|
||||
final FocusNode focusNode;
|
||||
|
||||
@override
|
||||
State<MobileSearchTextfield> createState() => _MobileSearchTextfieldState();
|
||||
}
|
||||
|
||||
class _MobileSearchTextfieldState extends State<MobileSearchTextfield> {
|
||||
late final TextEditingController controller;
|
||||
final ValueNotifier<bool> hasFocusValueNotifier = ValueNotifier(true);
|
||||
|
||||
FocusNode get focusNode => widget.focusNode;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
controller = TextEditingController(text: widget.query);
|
||||
focusNode.addListener(onFocusChanged);
|
||||
controller.addListener(() {
|
||||
if (!mounted) return;
|
||||
widget.onChanged?.call(controller.text);
|
||||
});
|
||||
bottomNavigationBarItemType.addListener(onBackOrLeave);
|
||||
focusNode.requestFocus();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
controller.dispose();
|
||||
focusNode.removeListener(onFocusChanged);
|
||||
hasFocusValueNotifier.dispose();
|
||||
bottomNavigationBarItemType.removeListener(onBackOrLeave);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = AppFlowyTheme.of(context);
|
||||
return SizedBox(
|
||||
height: 42,
|
||||
child: ValueListenableBuilder(
|
||||
valueListenable: controller,
|
||||
builder: (context, _, __) {
|
||||
final hasText = controller.text.isNotEmpty;
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
autovalidateMode: AutovalidateMode.onUserInteraction,
|
||||
autofocus: true,
|
||||
focusNode: focusNode,
|
||||
textAlign: TextAlign.left,
|
||||
controller: controller,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
height: 20 / 14,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: theme.textColorScheme.primary,
|
||||
),
|
||||
decoration: buildInputDecoration(context),
|
||||
),
|
||||
),
|
||||
ValueListenableBuilder(
|
||||
valueListenable: hasFocusValueNotifier,
|
||||
builder: (context, hasFocus, __) {
|
||||
if (!hasFocus || !hasText) return SizedBox.shrink();
|
||||
return GestureDetector(
|
||||
onTap: () => focusNode.unfocus(),
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: Container(
|
||||
height: 42,
|
||||
padding: EdgeInsets.only(left: 8),
|
||||
child: Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
LocaleKeys.button_cancel.tr(),
|
||||
style: theme.textStyle.body
|
||||
.standard(color: theme.textColorScheme.action),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
InputDecoration buildInputDecoration(BuildContext context) {
|
||||
final theme = AppFlowyTheme.of(context);
|
||||
final showCancelIcon = controller.text.isNotEmpty;
|
||||
final border = OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(10.0)),
|
||||
borderSide: BorderSide(color: theme.borderColorScheme.greyTertiary),
|
||||
);
|
||||
final enableBorder = border.copyWith(
|
||||
borderSide: BorderSide(color: theme.borderColorScheme.themeThick),
|
||||
);
|
||||
final hintStyle = TextStyle(
|
||||
fontSize: 14,
|
||||
height: 20 / 14,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: theme.textColorScheme.tertiary,
|
||||
);
|
||||
return InputDecoration(
|
||||
hintText: widget.hintText,
|
||||
hintStyle: hintStyle,
|
||||
contentPadding: const EdgeInsets.fromLTRB(8, 10, 8, 10),
|
||||
isDense: true,
|
||||
border: border,
|
||||
enabledBorder: border,
|
||||
focusedBorder: enableBorder,
|
||||
prefixIconConstraints: BoxConstraints.loose(Size(34, 40)),
|
||||
prefixIcon: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(8, 10, 4, 10),
|
||||
child: FlowySvg(
|
||||
FlowySvgs.m_home_search_icon_m,
|
||||
color: theme.iconColorScheme.secondary,
|
||||
size: Size.square(20),
|
||||
),
|
||||
),
|
||||
suffixIconConstraints:
|
||||
showCancelIcon ? BoxConstraints.loose(Size(34, 40)) : null,
|
||||
suffixIcon: showCancelIcon
|
||||
? GestureDetector(
|
||||
onTap: () {
|
||||
controller.clear();
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(4, 10, 8, 10),
|
||||
child: FlowySvg(
|
||||
FlowySvgs.clear_s,
|
||||
blendMode: null,
|
||||
color: theme.iconColorScheme.tertiary,
|
||||
size: Size.square(20),
|
||||
),
|
||||
),
|
||||
)
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
void onFocusChanged() {
|
||||
if (!mounted) return;
|
||||
hasFocusValueNotifier.value = focusNode.hasFocus;
|
||||
}
|
||||
|
||||
void onBackOrLeave() {
|
||||
final label = bottomNavigationBarItemType.value;
|
||||
if (label == BottomNavigationBarItemType.search.label) {
|
||||
focusNode.requestFocus();
|
||||
} else {
|
||||
focusNode.unfocus();
|
||||
controller.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
import 'view_ancestor_cache.dart';
|
||||
part 'mobile_view_ancestors.freezed.dart';
|
||||
|
||||
class ViewAncestorBloc extends Bloc<ViewAncestorEvent, ViewAncestorState> {
|
||||
ViewAncestorBloc(String viewId) : super(ViewAncestorState.initial(viewId)) {
|
||||
_cache = getIt<ViewAncestorCache>();
|
||||
_dispatch();
|
||||
}
|
||||
|
||||
late final ViewAncestorCache _cache;
|
||||
|
||||
void _dispatch() {
|
||||
on<ViewAncestorEvent>(
|
||||
(event, emit) async {
|
||||
await event.map(
|
||||
initial: (e) async {
|
||||
final ancester = await _cache.getAncestor(
|
||||
state.viewId,
|
||||
onRefresh: (ancestor) {
|
||||
if (!emit.isDone) {
|
||||
emit(state.copyWith(ancestor: ancestor, isLoading: false));
|
||||
}
|
||||
},
|
||||
);
|
||||
emit(state.copyWith(ancestor: ancester, isLoading: false));
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
add(const ViewAncestorEvent.initial());
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
class ViewAncestorEvent with _$ViewAncestorEvent {
|
||||
const factory ViewAncestorEvent.initial() = Initial;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class ViewAncestorState with _$ViewAncestorState {
|
||||
const factory ViewAncestorState({
|
||||
required ViewAncestor ancestor,
|
||||
required String viewId,
|
||||
@Default(true) bool isLoading,
|
||||
}) = _ViewAncestorState;
|
||||
|
||||
factory ViewAncestorState.initial(String viewId) => ViewAncestorState(
|
||||
viewId: viewId,
|
||||
ancestor: ViewAncestor.empty(),
|
||||
);
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:appflowy/workspace/application/view/view_ext.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_service.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:appflowy_result/appflowy_result.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ViewAncestorCache {
|
||||
ViewAncestorCache();
|
||||
|
||||
final Map<String, ViewAncestor> _ancestors = {};
|
||||
|
||||
Future<ViewAncestor> getAncestor(
|
||||
String viewId, {
|
||||
ValueChanged<ViewAncestor>? onRefresh,
|
||||
}) async {
|
||||
final cachedAncestor = _ancestors[viewId];
|
||||
if (cachedAncestor != null) {
|
||||
unawaited(_getAncestor(viewId, onRefresh: onRefresh));
|
||||
return cachedAncestor;
|
||||
}
|
||||
return _getAncestor(viewId);
|
||||
}
|
||||
|
||||
Future<ViewAncestor> _getAncestor(
|
||||
String viewId, {
|
||||
ValueChanged<ViewAncestor>? onRefresh,
|
||||
}) async {
|
||||
final List<ViewPB> ancestors =
|
||||
await ViewBackendService.getViewAncestors(viewId).fold(
|
||||
(s) => s.items
|
||||
.where((e) => e.parentViewId.isNotEmpty && e.id != viewId)
|
||||
.toList(),
|
||||
(f) => [],
|
||||
);
|
||||
final newAncestors = ViewAncestor(
|
||||
ancestors: ancestors.map((e) => ViewParent.fromViewPB(e)).toList(),
|
||||
);
|
||||
_ancestors[viewId] = newAncestors;
|
||||
onRefresh?.call(newAncestors);
|
||||
return newAncestors;
|
||||
}
|
||||
}
|
||||
|
||||
class ViewAncestor {
|
||||
const ViewAncestor({required this.ancestors});
|
||||
|
||||
const ViewAncestor.empty() : ancestors = const [];
|
||||
|
||||
final List<ViewParent> ancestors;
|
||||
}
|
||||
|
||||
class ViewParent {
|
||||
ViewParent({required this.id, required this.name});
|
||||
|
||||
final String id;
|
||||
final String name;
|
||||
|
||||
static ViewParent fromViewPB(ViewPB view) =>
|
||||
ViewParent(id: view.id, name: view.nameOrDefault);
|
||||
}
|
||||
+31
-48
@@ -1,4 +1,5 @@
|
||||
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';
|
||||
@@ -70,7 +71,6 @@ class MobileSelectionMenu extends SelectionMenuService {
|
||||
final editorWidth = editorState.renderBox!.size.width;
|
||||
|
||||
_positionNotifier = ValueNotifier(position);
|
||||
final showAtTop = position.top != null;
|
||||
_selectionMenuEntry = OverlayEntry(
|
||||
builder: (context) {
|
||||
return SizedBox(
|
||||
@@ -94,7 +94,6 @@ class MobileSelectionMenu extends SelectionMenuService {
|
||||
child: MobileSelectionMenuWidget(
|
||||
selectionMenuStyle: style,
|
||||
singleColumn: singleColumn,
|
||||
showAtTop: showAtTop,
|
||||
items: selectionMenuItems
|
||||
..forEach((element) {
|
||||
if (element is MobileSelectionMenuItem) {
|
||||
@@ -167,8 +166,7 @@ class MobileSelectionMenu extends SelectionMenuService {
|
||||
if (selectionRects.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
final screenSize = MediaQuery.of(context).size;
|
||||
calculateSelectionMenuOffset(selectionRects.first, screenSize);
|
||||
calculateSelectionMenuOffset(selectionRects.first);
|
||||
final (left, top, right, bottom) = getPosition();
|
||||
return _Position(left, top, right, bottom);
|
||||
}
|
||||
@@ -207,65 +205,50 @@ class MobileSelectionMenu extends SelectionMenuService {
|
||||
return (left, top, right, bottom);
|
||||
}
|
||||
|
||||
void calculateSelectionMenuOffset(Rect rect, Size screenSize) {
|
||||
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;
|
||||
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 screenHeight = screenSize.height;
|
||||
final editorWidth = editorState.renderBox!.size.width;
|
||||
final rectHeight = rect.height;
|
||||
|
||||
// show below default
|
||||
_alignment = Alignment.bottomRight;
|
||||
final bottomRight = rect.topLeft;
|
||||
final offset = bottomRight;
|
||||
final limitX = editorWidth + editorOffset.dx - menuWidth,
|
||||
limitY = screenHeight -
|
||||
editorHeight +
|
||||
editorOffset.dy -
|
||||
menuHeight -
|
||||
rectHeight;
|
||||
_alignment = Alignment.topLeft;
|
||||
final bottomRight = rect.bottomRight;
|
||||
final topRight = rect.topRight;
|
||||
var offset = bottomRight + menuOffset;
|
||||
final limitX = editorWidth - menuWidth + editorOffset.dx;
|
||||
_offset = Offset(
|
||||
editorWidth - offset.dx - menuWidth,
|
||||
screenHeight - offset.dy - menuHeight - rectHeight,
|
||||
min(offset.dx, limitX),
|
||||
offset.dy,
|
||||
);
|
||||
|
||||
// show above
|
||||
if (offset.dy + menuHeight >= editorOffset.dy + editorHeight) {
|
||||
/// show above
|
||||
if (offset.dy > menuHeight) {
|
||||
_offset = Offset(
|
||||
_offset.dx,
|
||||
offset.dy - menuHeight,
|
||||
);
|
||||
_alignment = Alignment.topRight;
|
||||
} else {
|
||||
_offset = Offset(
|
||||
_offset.dx,
|
||||
limitY,
|
||||
);
|
||||
}
|
||||
offset = topRight - menuOffset;
|
||||
_alignment = Alignment.bottomLeft;
|
||||
|
||||
_offset = Offset(
|
||||
offset.dx,
|
||||
editorOffset.dy + editorHeight - offset.dy,
|
||||
);
|
||||
}
|
||||
|
||||
if (offset.dx + menuWidth >= editorOffset.dx + editorWidth) {
|
||||
/// show left
|
||||
if (offset.dx > menuWidth) {
|
||||
_alignment = _alignment == Alignment.bottomRight
|
||||
? Alignment.bottomLeft
|
||||
: Alignment.topLeft;
|
||||
_offset = Offset(
|
||||
offset.dx - menuWidth,
|
||||
_offset.dy,
|
||||
);
|
||||
} else {
|
||||
_offset = Offset(
|
||||
limitX,
|
||||
_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;
|
||||
_offset = Offset(
|
||||
min(x, limitX),
|
||||
_offset.dy,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+21
-38
@@ -2,7 +2,6 @@ import 'dart:math';
|
||||
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:appflowy_ui/appflowy_ui.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
@@ -23,7 +22,6 @@ class MobileSelectionMenuWidget extends StatefulWidget {
|
||||
required this.deleteSlashByDefault,
|
||||
required this.singleColumn,
|
||||
required this.startOffset,
|
||||
required this.showAtTop,
|
||||
this.nameBuilder,
|
||||
});
|
||||
|
||||
@@ -40,7 +38,6 @@ class MobileSelectionMenuWidget extends StatefulWidget {
|
||||
|
||||
final bool deleteSlashByDefault;
|
||||
final bool singleColumn;
|
||||
final bool showAtTop;
|
||||
final int startOffset;
|
||||
|
||||
final SelectionMenuItemNameBuilder? nameBuilder;
|
||||
@@ -175,37 +172,27 @@ class _MobileSelectionMenuWidgetState extends State<MobileSelectionMenuWidget> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
height: 192,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (widget.showAtTop) Spacer(),
|
||||
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,
|
||||
),
|
||||
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),
|
||||
),
|
||||
),
|
||||
if (!widget.showAtTop) Spacer(),
|
||||
],
|
||||
],
|
||||
borderRadius: BorderRadius.circular(6.0),
|
||||
),
|
||||
child: _showingItems.isEmpty
|
||||
? _buildNoResultsWidget(context)
|
||||
: _buildResultsWidget(
|
||||
context,
|
||||
_showingItems,
|
||||
widget.itemCountFilter,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -329,7 +316,6 @@ class _MobileSelectionMenuWidgetState extends State<MobileSelectionMenuWidget> {
|
||||
}
|
||||
|
||||
Widget _buildNoResultsWidget(BuildContext context) {
|
||||
final theme = AppFlowyTheme.of(context);
|
||||
return DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).cardColor,
|
||||
@@ -352,10 +338,7 @@ class _MobileSelectionMenuWidgetState extends State<MobileSelectionMenuWidget> {
|
||||
child: Center(
|
||||
child: Text(
|
||||
LocaleKeys.inlineActions_noResults.tr(),
|
||||
style: TextStyle(
|
||||
fontSize: 18.0,
|
||||
color: theme.textColorScheme.primary,
|
||||
),
|
||||
style: TextStyle(fontSize: 18.0, color: Color(0x801F2225)),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
|
||||
+10
-11
@@ -1,9 +1,9 @@
|
||||
import 'package:appflowy/core/helpers/url_launcher.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/mobile/presentation/setting/widgets/mobile_setting_trailing.dart';
|
||||
import 'package:appflowy/startup/tasks/device_info_task.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/feature_flags/mobile_feature_flag_screen.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
@@ -22,23 +22,23 @@ class AboutSettingGroup extends StatelessWidget {
|
||||
settingItemList: [
|
||||
MobileSettingItem(
|
||||
name: LocaleKeys.settings_mobile_privacyPolicy.tr(),
|
||||
trailing: MobileSettingTrailing(
|
||||
text: '',
|
||||
trailing: const Icon(
|
||||
Icons.chevron_right,
|
||||
),
|
||||
onTap: () => afLaunchUrlString('https://appflowy.com/privacy'),
|
||||
),
|
||||
MobileSettingItem(
|
||||
name: LocaleKeys.settings_mobile_termsAndConditions.tr(),
|
||||
trailing: MobileSettingTrailing(
|
||||
text: '',
|
||||
trailing: const Icon(
|
||||
Icons.chevron_right,
|
||||
),
|
||||
onTap: () => afLaunchUrlString('https://appflowy.com/terms'),
|
||||
),
|
||||
if (kDebugMode)
|
||||
MobileSettingItem(
|
||||
name: 'Feature Flags',
|
||||
trailing: MobileSettingTrailing(
|
||||
text: '',
|
||||
trailing: const Icon(
|
||||
Icons.chevron_right,
|
||||
),
|
||||
onTap: () {
|
||||
context.push(FeatureFlagScreen.routeName);
|
||||
@@ -46,10 +46,9 @@ class AboutSettingGroup extends StatelessWidget {
|
||||
),
|
||||
MobileSettingItem(
|
||||
name: LocaleKeys.settings_mobile_version.tr(),
|
||||
trailing: MobileSettingTrailing(
|
||||
text:
|
||||
'${ApplicationInfo.applicationVersion} (${ApplicationInfo.buildNumber})',
|
||||
showArrow: false,
|
||||
trailing: FlowyText(
|
||||
'${ApplicationInfo.applicationVersion} (${ApplicationInfo.buildNumber})',
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
+17
-3
@@ -2,11 +2,11 @@ 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/setting/widgets/mobile_setting_trailing.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: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';
|
||||
@@ -23,6 +23,7 @@ class AiSettingsGroup extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return BlocProvider(
|
||||
create: (context) => SettingsAIBloc(
|
||||
userProfile,
|
||||
@@ -35,8 +36,21 @@ class AiSettingsGroup extends StatelessWidget {
|
||||
settingItemList: [
|
||||
MobileSettingItem(
|
||||
name: LocaleKeys.settings_aiPage_keys_llmModelType.tr(),
|
||||
trailing: MobileSettingTrailing(
|
||||
text: state.availableModels?.selectedModel.name ?? "",
|
||||
trailing: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 200),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Flexible(
|
||||
child: FlowyText(
|
||||
state.availableModels?.selectedModel.name ?? "",
|
||||
color: theme.colorScheme.onSurface,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
const Icon(Icons.chevron_right),
|
||||
],
|
||||
),
|
||||
),
|
||||
onTap: () => _onLLMModelTypeTap(context, state),
|
||||
),
|
||||
|
||||
Loaded 100 of 888 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user