Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fe5ce75ea8 | ||
|
|
99b2b2712b | ||
|
|
96af012471 | ||
|
|
7ad05feb5b | ||
|
|
69d3181a64 | ||
|
|
109ebaf989 | ||
|
|
1a13de2ee9 | ||
|
|
48d6967d3b | ||
|
|
7d55153475 | ||
|
|
d25830aece | ||
|
|
04eea26a55 | ||
|
|
25e94da7e7 | ||
|
|
2d7a373d77 | ||
|
|
1491fbfb0d | ||
|
|
0d776a9294 | ||
|
|
d4cef2e866 | ||
|
|
a070ed2441 | ||
|
|
5fa441cbf5 | ||
|
|
4ae679128f | ||
|
|
4d71b42a3d | ||
|
|
ff3ff9f888 | ||
|
|
147080ad27 | ||
|
|
bb4b987681 | ||
|
|
415f121720 | ||
|
|
9824d5980a | ||
|
|
5ff6405f7f | ||
|
|
a4e0c5754f | ||
|
|
5f94ba129e | ||
|
|
af07b53484 | ||
|
|
b43b522d97 | ||
|
|
0683483fd4 | ||
|
|
afab3d5374 | ||
|
|
066bba95f5 | ||
|
|
d004c94a2f | ||
|
|
c0ae803280 | ||
|
|
d7a67c0efb | ||
|
|
b307dd30d8 | ||
|
|
068245eec9 | ||
|
|
6b4d4fef15 | ||
|
|
64aa2ba7e4 | ||
|
|
8665a25b39 | ||
|
|
551e012147 | ||
|
|
7da759c662 | ||
|
|
8036d070ad | ||
|
|
a5ed145aa3 | ||
|
|
20b485bcfe | ||
|
|
38e3947b2d | ||
|
|
94f9349aba | ||
|
|
a9ea8fd159 | ||
|
|
1e6762b424 | ||
|
|
7c54b6d599 | ||
|
|
3e17613f54 | ||
|
|
9d61ca0278 | ||
|
|
3595de5e12 | ||
|
|
32ca864bf7 | ||
|
|
bfa9233bd4 | ||
|
|
c659cf4ff2 | ||
|
|
cac3acd553 | ||
|
|
771dd9979f | ||
|
|
929508df16 | ||
|
|
79b1515c3a | ||
|
|
7fb1b4f43f | ||
|
|
b3dd5fb8bd | ||
|
|
29a6eab1ce | ||
|
|
c475a5d960 | ||
|
|
414bb26e56 | ||
|
|
1fad713477 | ||
|
|
e18e031710 | ||
|
|
bddaac05ae | ||
|
|
3e5fae002b | ||
|
|
66835a5409 | ||
|
|
8afbf28430 | ||
|
|
0427402ba7 | ||
|
|
6973d66263 | ||
|
|
5fea801a88 | ||
|
|
8b81889bcf | ||
|
|
800094587b | ||
|
|
de75e028df | ||
|
|
9b3c1921e8 | ||
|
|
412f34c72a | ||
|
|
a7364e1f4a | ||
|
|
16467e9c13 | ||
|
|
b00d29d0cd | ||
|
|
acc951c5eb | ||
|
|
b9ecc7ceb6 | ||
|
|
6f83f41c2d | ||
|
|
8179419f8b | ||
|
|
4a1a143a66 | ||
|
|
21bbf87089 | ||
|
|
19d9e7717c | ||
|
|
d190850f03 | ||
|
|
68de83c611 | ||
|
|
729b8571b5 | ||
|
|
31e9a0b4ea | ||
|
|
6a9866b9d2 | ||
|
|
1c500fbfc5 | ||
|
|
4992f9c281 | ||
|
|
75c26c807c |
No files matched your search
@@ -23,7 +23,7 @@ on:
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
FLUTTER_VERSION: "3.10.4"
|
||||
FLUTTER_VERSION: "3.13.9"
|
||||
RUST_TOOLCHAIN: "1.70"
|
||||
CARGO_MAKE_VERSION: "0.36.6"
|
||||
|
||||
@@ -108,21 +108,19 @@ jobs:
|
||||
|
||||
- name: Build AppFlowy
|
||||
working-directory: frontend
|
||||
run: |
|
||||
cargo make --profile ${{ matrix.flutter_profile }} appflowy-core-dev
|
||||
run: cargo make --profile ${{ matrix.flutter_profile }} appflowy-core-dev
|
||||
|
||||
- name: Run code generation
|
||||
working-directory: frontend
|
||||
run: |
|
||||
cargo make code_generation
|
||||
run: cargo make code_generation
|
||||
|
||||
- name: Flutter Analyzer
|
||||
working-directory: frontend/appflowy_flutter
|
||||
run: |
|
||||
flutter analyze .
|
||||
run: flutter analyze .
|
||||
|
||||
- name: Compress appflowy_flutter
|
||||
run: tar -czf appflowy_flutter.tar.gz frontend/appflowy_flutter
|
||||
run: |
|
||||
tar -czf appflowy_flutter.tar.gz frontend/appflowy_flutter
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
with:
|
||||
@@ -135,11 +133,11 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [macos-latest]
|
||||
os: [ubuntu-latest]
|
||||
include:
|
||||
- os: macos-latest
|
||||
flutter_profile: development-mac-x86_64
|
||||
target: x86_64-apple-darwin
|
||||
- os: ubuntu-latest
|
||||
flutter_profile: development-linux-x86_64
|
||||
target: x86_64-unknown-linux-gnu
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
steps:
|
||||
@@ -176,36 +174,63 @@ jobs:
|
||||
- name: Install prerequisites
|
||||
working-directory: frontend
|
||||
run: |
|
||||
if [ "$RUNNER_OS" == "macOS" ]; then
|
||||
cargo make appflowy-flutter-deps-tools
|
||||
if [ "$RUNNER_OS" == "Linux" ]; then
|
||||
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
|
||||
fi
|
||||
shell: bash
|
||||
|
||||
- name: Enable Flutter Desktop
|
||||
run: |
|
||||
if [ "$RUNNER_OS" == "Linux" ]; then
|
||||
flutter config --enable-linux-desktop
|
||||
elif [ "$RUNNER_OS" == "macOS" ]; then
|
||||
flutter config --enable-macos-desktop
|
||||
elif [ "$RUNNER_OS" == "Windows" ]; then
|
||||
git config --system core.longpaths true
|
||||
flutter config --enable-windows-desktop
|
||||
fi
|
||||
shell: bash
|
||||
|
||||
- uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: ${{ github.run_id }}-${{ matrix.os }}
|
||||
|
||||
- name: Uncompress appflowy_flutter
|
||||
run: tar -xf appflowy_flutter.tar.gz
|
||||
|
||||
- name: Run flutter pub get
|
||||
working-directory: frontend
|
||||
run: cargo make pub_get
|
||||
|
||||
- name: Run Flutter unit tests
|
||||
working-directory: frontend
|
||||
run: |
|
||||
if [ "$RUNNER_OS" == "macOS" ]; then
|
||||
flutter config --enable-macos-desktop
|
||||
cargo make --profile ${{ matrix.flutter_profile }} appflowy-dev
|
||||
cargo make dart_unit_test
|
||||
else
|
||||
elif [ "$RUNNER_OS" == "Linux" ]; then
|
||||
cargo make dart_unit_test_no_build
|
||||
elif [ "$RUNNER_OS" == "Windows" ]; then
|
||||
cargo make dart_unit_test_no_build
|
||||
fi
|
||||
shell: bash
|
||||
|
||||
- name: Upload coverage to Codecov
|
||||
uses: Wandalen/wretry.action@v1.0.36
|
||||
with:
|
||||
action: codecov/codecov-action@v3
|
||||
with: |
|
||||
name: appflowy
|
||||
flags: appflowy_flutter_unit_test
|
||||
fail_ci_if_error: true
|
||||
verbose: true
|
||||
os: ${{ matrix.os }}
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
attempt_limit: 20
|
||||
attempt_delay: 10000
|
||||
# disable coverage for now as it's not working
|
||||
# - name: Upload coverage to Codecov
|
||||
# uses: Wandalen/wretry.action@v1.0.36
|
||||
# with:
|
||||
# action: codecov/codecov-action@v3
|
||||
# with: |
|
||||
# name: appflowy
|
||||
# flags: appflowy_flutter_unit_test
|
||||
# fail_ci_if_error: true
|
||||
# verbose: true
|
||||
# os: ${{ matrix.os }}
|
||||
# token: ${{ secrets.CODECOV_TOKEN }}
|
||||
# attempt_limit: 20
|
||||
# attempt_delay: 10000
|
||||
|
||||
integration_test:
|
||||
needs: [prepare]
|
||||
@@ -213,7 +238,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest,windows-latest]
|
||||
os: [ubuntu-latest, windows-latest]
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
flutter_profile: development-linux-x86_64
|
||||
@@ -274,13 +299,12 @@ jobs:
|
||||
with:
|
||||
name: ${{ github.run_id }}-${{ matrix.os }}
|
||||
|
||||
- name: Uncompress appflowy_flutter
|
||||
- name: Uncompressed appflowy_flutter
|
||||
run: tar -xf appflowy_flutter.tar.gz
|
||||
|
||||
- name: Run code generation
|
||||
- name: Run flutter pub get
|
||||
working-directory: frontend
|
||||
run: |
|
||||
cargo make code_generation
|
||||
run: cargo make pub_get
|
||||
|
||||
- name: Run Flutter integration tests
|
||||
working-directory: frontend/appflowy_flutter
|
||||
@@ -297,19 +321,101 @@ jobs:
|
||||
fi
|
||||
shell: bash
|
||||
|
||||
- name: Upload coverage to Codecov
|
||||
uses: Wandalen/wretry.action@v1.0.36
|
||||
cloud_integration_test:
|
||||
needs: [prepare]
|
||||
# if: github.event_name != 'pull_request'
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest]
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
flutter_profile: development-linux-x86_64
|
||||
target: x86_64-unknown-linux-gnu
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
steps:
|
||||
- name: Checkout appflowy cloud code
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
action: codecov/codecov-action@v3
|
||||
with: |
|
||||
name: appflowy
|
||||
flags: appflowy_flutter_integrateion_test
|
||||
fail_ci_if_error: true
|
||||
verbose: true
|
||||
os: ${{ matrix.os }}
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
attempt_limit: 20
|
||||
attempt_delay: 10000
|
||||
repository: AppFlowy-IO/AppFlowy-Cloud
|
||||
path: AppFlowy-Cloud
|
||||
depth: 1
|
||||
|
||||
|
||||
- name: Prepare appflowy cloud env
|
||||
working-directory: AppFlowy-Cloud
|
||||
run: |
|
||||
# log level
|
||||
cp dev.env .env
|
||||
sed -i 's|RUST_LOG=.*|RUST_LOG=trace|' .env
|
||||
sed -i 's/GOTRUE_SMTP_USER=.*/GOTRUE_SMTP_USER=${{ secrets.INTEGRATION_TEST_GOTRUE_SMTP_USER }}/' .env
|
||||
sed -i 's/GOTRUE_SMTP_PASS=.*/GOTRUE_SMTP_PASS=${{ secrets.INTEGRATION_TEST_GOTRUE_SMTP_PASS }}/' .env
|
||||
sed -i 's/GOTRUE_SMTP_ADMIN_EMAIL=.*/GOTRUE_SMTP_ADMIN_EMAIL=${{ secrets.INTEGRATION_TEST_GOTRUE_SMTP_ADMIN_EMAIL }}/' .env
|
||||
sed -i 's/GOTRUE_EXTERNAL_GOOGLE_ENABLED=.*/GOTRUE_EXTERNAL_GOOGLE_ENABLED=true/' .env
|
||||
sed -i 's|API_EXTERNAL_URL=.*|API_EXTERNAL_URL=http://localhost|' .env
|
||||
cat .env
|
||||
|
||||
- name: Run Docker-Compose
|
||||
working-directory: AppFlowy-Cloud
|
||||
run: |
|
||||
docker compose up -d
|
||||
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Install flutter
|
||||
id: flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
channel: "stable"
|
||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||
|
||||
- uses: taiki-e/install-action@v2
|
||||
with:
|
||||
tool: cargo-make@${{ env.CARGO_MAKE_VERSION }}
|
||||
|
||||
- name: Install prerequisites
|
||||
working-directory: frontend
|
||||
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 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
|
||||
shell: bash
|
||||
|
||||
- name: Enable Flutter Desktop
|
||||
run: |
|
||||
flutter config --enable-linux-desktop
|
||||
shell: bash
|
||||
|
||||
- uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: ${{ github.run_id }}-${{ matrix.os }}
|
||||
|
||||
- name: Uncompressed appflowy_flutter
|
||||
run: |
|
||||
tar -xf appflowy_flutter.tar.gz
|
||||
ls -al
|
||||
|
||||
- name: Enable localhost env
|
||||
working-directory: frontend/appflowy_flutter
|
||||
run: |
|
||||
echo 'APPFLOWY_CLOUD_URL=http://localhost' >> .env
|
||||
dart run build_runner clean && dart run build_runner build --delete-conflicting-outputs
|
||||
|
||||
- name: Run flutter pub get
|
||||
working-directory: frontend
|
||||
run: cargo make pub_get
|
||||
|
||||
- name: Run Flutter integration tests
|
||||
working-directory: frontend/appflowy_flutter
|
||||
run: |
|
||||
export DISPLAY=:99
|
||||
sudo Xvfb -ac :99 -screen 0 1280x1024x24 > /dev/null 2>&1 &
|
||||
sudo apt-get install network-manager
|
||||
flutter test integration_test/cloud_runner.dart -d Linux --coverage
|
||||
shell: bash
|
||||
|
||||
build:
|
||||
needs: [prepare]
|
||||
@@ -381,15 +487,9 @@ jobs:
|
||||
with:
|
||||
name: ${{ github.run_id }}-${{ matrix.os }}
|
||||
|
||||
- name: Uncompress appflowy_flutter
|
||||
- name: Uncompressed appflowy_flutter
|
||||
run: tar -xf appflowy_flutter.tar.gz
|
||||
|
||||
- name: Run code generation
|
||||
if: matrix.os == 'windows-latest'
|
||||
working-directory: frontend
|
||||
run: |
|
||||
cargo make code_generation
|
||||
|
||||
- name: Build flutter product
|
||||
working-directory: frontend
|
||||
run: |
|
||||
|
||||
@@ -18,7 +18,7 @@ on:
|
||||
- "!frontend/appflowy_tauri/**"
|
||||
|
||||
env:
|
||||
FLUTTER_VERSION: "3.10.1"
|
||||
FLUTTER_VERSION: "3.13.9"
|
||||
RUST_TOOLCHAIN: "1.70"
|
||||
|
||||
concurrency:
|
||||
|
||||
@@ -6,7 +6,7 @@ on:
|
||||
- "*"
|
||||
|
||||
env:
|
||||
FLUTTER_VERSION: "3.10.1"
|
||||
FLUTTER_VERSION: "3.13.9"
|
||||
RUST_TOOLCHAIN: "1.70"
|
||||
|
||||
jobs:
|
||||
|
||||
@@ -11,7 +11,7 @@ on:
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
FLUTTER_VERSION: "3.10.1"
|
||||
FLUTTER_VERSION: "3.13.9"
|
||||
RUST_TOOLCHAIN: "1.70"
|
||||
|
||||
jobs:
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
"preLaunchTask": "AF: Build Appflowy Core",
|
||||
"env": {
|
||||
"RUST_LOG": "trace",
|
||||
"RUST_BACKTRACE": 1
|
||||
"RUST_BACKTRACE": "1"
|
||||
},
|
||||
"cwd": "${workspaceRoot}/appflowy_flutter"
|
||||
},
|
||||
@@ -44,6 +44,17 @@
|
||||
},
|
||||
"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",
|
||||
@@ -55,6 +66,27 @@
|
||||
},
|
||||
"cwd": "${workspaceRoot}/appflowy_flutter"
|
||||
},
|
||||
{
|
||||
"name": "AF-iOS-Simulator: Build Dart only",
|
||||
"request": "launch",
|
||||
"program": "./lib/main.dart",
|
||||
"type": "dart",
|
||||
"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",
|
||||
@@ -66,6 +98,17 @@
|
||||
},
|
||||
"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",
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
"type": "shell",
|
||||
"dependsOrder": "sequence",
|
||||
"dependsOn": [
|
||||
"AF: Dart Clean",
|
||||
"AF: Flutter Clean",
|
||||
"AF: Build Appflowy Core",
|
||||
"AF: Flutter Pub Get",
|
||||
@@ -33,7 +32,6 @@
|
||||
"type": "shell",
|
||||
"dependsOrder": "sequence",
|
||||
"dependsOn": [
|
||||
"AF: Dart Clean",
|
||||
"AF: Flutter Clean",
|
||||
"AF: Build Appflowy Core For iOS",
|
||||
"AF: Flutter Pub Get",
|
||||
@@ -52,7 +50,6 @@
|
||||
"type": "shell",
|
||||
"dependsOrder": "sequence",
|
||||
"dependsOn": [
|
||||
"AF: Dart Clean",
|
||||
"AF: Flutter Clean",
|
||||
"AF: Build Appflowy Core For iOS Simulator",
|
||||
"AF: Flutter Pub Get",
|
||||
@@ -89,7 +86,6 @@
|
||||
"type": "shell",
|
||||
"dependsOrder": "sequence",
|
||||
"dependsOn": [
|
||||
"AF: Dart Clean",
|
||||
"AF: Flutter Clean",
|
||||
"AF: Build Appflowy Core For Android",
|
||||
"AF: Flutter Pub Get",
|
||||
@@ -309,7 +305,7 @@
|
||||
{
|
||||
"label": "AF: Generate Env File",
|
||||
"type": "shell",
|
||||
"command": "dart run build_runner clean && dart run build_runner build --delete-conflicting-outputs ",
|
||||
"command": "dart run build_runner clean && dart run build_runner build --delete-conflicting-outputs",
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/appflowy_flutter"
|
||||
}
|
||||
|
||||
@@ -25,9 +25,10 @@ CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true
|
||||
CARGO_MAKE_CRATE_FS_NAME = "dart_ffi"
|
||||
CARGO_MAKE_CRATE_NAME = "dart-ffi"
|
||||
LIB_NAME = "dart_ffi"
|
||||
CURRENT_APP_VERSION = "0.3.8"
|
||||
CURRENT_APP_VERSION = "0.3.9"
|
||||
FLUTTER_DESKTOP_FEATURES = "dart,rev-sqlite"
|
||||
PRODUCT_NAME = "AppFlowy"
|
||||
MACOSX_DEPLOYMENT_TARGET = "11.0"
|
||||
# CRATE_TYPE: https://doc.rust-lang.org/reference/linkage.html
|
||||
# If you update the macOS's CRATE_TYPE, don't forget to update the
|
||||
# appflowy_backend.podspec
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<h1 align="center" style="margin:0"> AppFlowy_Flutter</h1>
|
||||
<div align="center">
|
||||
<img src="https://img.shields.io/badge/Flutter-v3.10.1-blue"/>
|
||||
<img src="https://img.shields.io/badge/Rust-v1.65-orange"/>
|
||||
<img src="https://img.shields.io/badge/Flutter-v3.13.19-blue"/>
|
||||
<img src="https://img.shields.io/badge/Rust-v1.70-orange"/>
|
||||
</div>
|
||||
|
||||
> Documentation for Contributors
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
android:name="${applicationName}">
|
||||
<activity android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:launchMode="singleTop"
|
||||
android:launchMode="singleInstance"
|
||||
android:theme="@style/LaunchTheme"
|
||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||
android:hardwareAccelerated="true" android:windowSoftInputMode="adjustResize">
|
||||
@@ -45,4 +45,6 @@
|
||||
-->
|
||||
<meta-data android:name="flutterEmbedding" android:value="2" />
|
||||
</application>
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
</manifest>
|
||||
@@ -3,7 +3,7 @@
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
int64_t init_sdk(char *path);
|
||||
int64_t init_sdk(char *data);
|
||||
|
||||
void async_event(int64_t port, const uint8_t *input, uintptr_t len);
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 5.4 KiB After Width: | Height: | Size: 5.9 KiB |
|
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 7.5 KiB After Width: | Height: | Size: 8.1 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 17 KiB |
@@ -1,41 +1 @@
|
||||
# Initial Setup
|
||||
|
||||
# 1. Copy the dev.env file to .env:
|
||||
# cp dev.env .env
|
||||
# Update the environment parameters as needed.
|
||||
|
||||
# 2. Generate the env.dart from this .env file:
|
||||
# You can use the "Generate Env File" task in VSCode.
|
||||
# Alternatively, execute the following commands:
|
||||
# cd appflowy_flutter
|
||||
# dart run build_runner clean && dart run build_runner build --delete-conflicting-outputs
|
||||
|
||||
|
||||
# Cloud Type Configuration
|
||||
# Use this configuration file to specify the cloud type and its associated settings. The available cloud types are:
|
||||
# Local: 0
|
||||
# Supabase: 1
|
||||
# AppFlowy Cloud: 2
|
||||
# By default, it's set to Local.
|
||||
CLOUD_TYPE=0
|
||||
|
||||
# Supabase Configuration
|
||||
# If using Supabase (CLOUD_TYPE=1), provide the following details:
|
||||
SUPABASE_URL=
|
||||
SUPABASE_ANON_KEY=
|
||||
|
||||
# AppFlowy Cloud Configuration
|
||||
# If using AppFlowy Cloud (CLOUD_TYPE=2), provide the following details:
|
||||
# For instance:
|
||||
# APPFLOWY_CLOUD_BASE_URL=https://xxxxxxxxx
|
||||
# APPFLOWY_CLOUD_WS_BASE_URL=wss://xxxxxxxxx
|
||||
# APPFLOWY_CLOUD_GOTRUE_URL=https://xxxxxxxxx
|
||||
#
|
||||
# Local host machine(For local develop)
|
||||
# APPFLOWY_CLOUD_BASE_URL=http://localhost:8000
|
||||
# APPFLOWY_CLOUD_WS_BASE_URL=ws://localhost:8000/ws
|
||||
# APPFLOWY_CLOUD_GOTRUE_URL=http://localhost:9998
|
||||
|
||||
APPFLOWY_CLOUD_BASE_URL=
|
||||
APPFLOWY_CLOUD_WS_BASE_URL=
|
||||
APPFLOWY_CLOUD_GOTRUE_URL=
|
||||
APPFLOWY_CLOUD_URL=
|
||||
@@ -0,0 +1,5 @@
|
||||
|
||||
SUPABASE_URL=
|
||||
SUPABASE_ANON_KEY=
|
||||
|
||||
APPFLOWY_CLOUD_URL=
|
||||
@@ -4,6 +4,7 @@ import 'package:appflowy/workspace/presentation/settings/widgets/settings_appear
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import 'util/util.dart';
|
||||
|
||||
void main() {
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
// ignore_for_file: unused_import
|
||||
|
||||
import 'package:appflowy/env/cloud_env.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/user/application/auth/af_cloud_mock_auth_service.dart';
|
||||
import 'package:appflowy/user/application/auth/auth_service.dart';
|
||||
import 'package:appflowy/workspace/application/settings/prelude.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/settings_user_view.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/uuid.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
import '../util/mock/mock_file_picker.dart';
|
||||
import '../util/util.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('appflowy cloud auth', () {
|
||||
testWidgets('sign in', (tester) async {
|
||||
await tester.initializeAppFlowy(
|
||||
cloudType: AuthenticatorType.appflowyCloud,
|
||||
);
|
||||
await tester.tapGoogleLoginInButton();
|
||||
tester.expectToSeeHomePage();
|
||||
});
|
||||
|
||||
testWidgets('sign out', (tester) async {
|
||||
await tester.initializeAppFlowy(
|
||||
cloudType: AuthenticatorType.appflowyCloud,
|
||||
);
|
||||
await tester.tapGoogleLoginInButton();
|
||||
|
||||
// Open the setting page and sign out
|
||||
await tester.openSettings();
|
||||
await tester.openSettingsPage(SettingsPage.user);
|
||||
await tester.tapButton(find.byType(SettingLogoutButton));
|
||||
|
||||
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: 1));
|
||||
tester.expectToSeeGoogleLoginButton();
|
||||
});
|
||||
|
||||
testWidgets('sign in as annoymous', (tester) async {
|
||||
await tester.initializeAppFlowy(
|
||||
cloudType: AuthenticatorType.appflowyCloud,
|
||||
);
|
||||
await tester.tapSignInAsGuest();
|
||||
|
||||
// should not see the sync setting page when sign in as annoymous
|
||||
await tester.openSettings();
|
||||
await tester.openSettingsPage(SettingsPage.user);
|
||||
tester.expectToSeeGoogleLoginButton();
|
||||
});
|
||||
|
||||
testWidgets('enable sync', (tester) async {
|
||||
await tester.initializeAppFlowy(
|
||||
cloudType: AuthenticatorType.appflowyCloud,
|
||||
);
|
||||
|
||||
await tester.tapGoogleLoginInButton();
|
||||
// Open the setting page and sign out
|
||||
await tester.openSettings();
|
||||
await tester.openSettingsPage(SettingsPage.cloud);
|
||||
|
||||
// the switch should be on by default
|
||||
tester.assertAppFlowyCloudEnableSyncSwitchValue(true);
|
||||
await tester.toggleEnableSync(AppFlowyCloudEnableSync);
|
||||
|
||||
// the switch should be off
|
||||
tester.assertAppFlowyCloudEnableSyncSwitchValue(false);
|
||||
|
||||
// the switch should be on after toggling
|
||||
await tester.toggleEnableSync(AppFlowyCloudEnableSync);
|
||||
tester.assertAppFlowyCloudEnableSyncSwitchValue(true);
|
||||
});
|
||||
|
||||
// testWidgets('custom folder sign in', (tester) async {
|
||||
// const userA = 'UserA';
|
||||
// final userAEmail = "${uuid()}@appflowy.io";
|
||||
// final initialPath = p.join(userA, appFlowyDataFolder);
|
||||
// final context = await tester.initializeAppFlowy(
|
||||
// cloudType: AuthenticatorType.appflowyCloud,
|
||||
// pathExtension: initialPath,
|
||||
// );
|
||||
// getIt.registerFactory<AuthService>(
|
||||
// () => AppFlowyCloudMockAuthService(
|
||||
// email: userAEmail,
|
||||
// ),
|
||||
// );
|
||||
// // remove the last extension
|
||||
// final rootPath = context.applicationDataDirectory.replaceFirst(
|
||||
// initialPath,
|
||||
// '',
|
||||
// );
|
||||
// await tester.tapGoogleLoginInButton();
|
||||
|
||||
// // Open the setting page and sign out
|
||||
// await tester.openSettings();
|
||||
// await tester.openSettingsPage(SettingsPage.user);
|
||||
// await tester.enterUserName(userA);
|
||||
|
||||
// await tester.openSettingsPage(SettingsPage.files);
|
||||
// await tester.pumpAndSettle();
|
||||
|
||||
// // mock the file_picker result
|
||||
// await mockGetDirectoryPath(
|
||||
// p.join(rootPath, "random_folder"),
|
||||
// );
|
||||
|
||||
// // after selecting the folder, an annoymous user should be signed in
|
||||
// await tester.tapCustomLocationButton();
|
||||
// tester.expectToSeeHomePage();
|
||||
// await tester.pumpAndSettle();
|
||||
|
||||
// // Login as userA in custom folder
|
||||
// await tester.openSettings();
|
||||
// await tester.openSettingsPage(SettingsPage.user);
|
||||
// await tester.tapGoogleLoginInButton();
|
||||
|
||||
// await tester.pumpAndSettle(const Duration(seconds: 1));
|
||||
// tester.expectToSeeHomePage();
|
||||
// // UserA should be displayed
|
||||
// tester.expectToSeeUserName(userA);
|
||||
// });
|
||||
});
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
import 'package:appflowy/env/cloud_env.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/workspace/application/settings/prelude.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/setting_supabase_cloud.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/settings_user_view.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
@@ -9,15 +11,15 @@ import '../util/util.dart';
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('auth', () {
|
||||
group('supabase auth', () {
|
||||
testWidgets('sign in with supabase', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.initializeAppFlowy(cloudType: AuthenticatorType.supabase);
|
||||
await tester.tapGoogleLoginInButton();
|
||||
tester.expectToSeeHomePage();
|
||||
});
|
||||
|
||||
testWidgets('sign out with supabase', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.initializeAppFlowy(cloudType: AuthenticatorType.supabase);
|
||||
await tester.tapGoogleLoginInButton();
|
||||
|
||||
// Open the setting page and sign out
|
||||
@@ -34,52 +36,53 @@ void main() {
|
||||
});
|
||||
|
||||
testWidgets('sign in as annoymous', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.initializeAppFlowy(cloudType: AuthenticatorType.supabase);
|
||||
await tester.tapSignInAsGuest();
|
||||
|
||||
// should not see the sync setting page when sign in as annoymous
|
||||
await tester.openSettings();
|
||||
await tester.expectNoSettingsPage(SettingsPage.syncSetting);
|
||||
await tester.openSettingsPage(SettingsPage.user);
|
||||
tester.expectToSeeGoogleLoginButton();
|
||||
});
|
||||
|
||||
testWidgets('enable encryption', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoogleLoginInButton();
|
||||
// testWidgets('enable encryption', (tester) async {
|
||||
// await tester.initializeAppFlowy(cloudType: CloudType.supabase);
|
||||
// await tester.tapGoogleLoginInButton();
|
||||
|
||||
// Open the setting page and sign out
|
||||
await tester.openSettings();
|
||||
await tester.openSettingsPage(SettingsPage.syncSetting);
|
||||
// // Open the setting page and sign out
|
||||
// await tester.openSettings();
|
||||
// await tester.openSettingsPage(SettingsPage.cloud);
|
||||
|
||||
// the switch should be off by default
|
||||
tester.assertEnableEncryptSwitchValue(false);
|
||||
await tester.toggleEnableEncrypt();
|
||||
// // the switch should be off by default
|
||||
// tester.assertEnableEncryptSwitchValue(false);
|
||||
// await tester.toggleEnableEncrypt();
|
||||
|
||||
// the switch should be on after toggling
|
||||
tester.assertEnableEncryptSwitchValue(true);
|
||||
// // the switch should be on after toggling
|
||||
// tester.assertEnableEncryptSwitchValue(true);
|
||||
|
||||
// the switch can not be toggled back to off
|
||||
await tester.toggleEnableEncrypt();
|
||||
tester.assertEnableEncryptSwitchValue(true);
|
||||
});
|
||||
// // the switch can not be toggled back to off
|
||||
// await tester.toggleEnableEncrypt();
|
||||
// tester.assertEnableEncryptSwitchValue(true);
|
||||
// });
|
||||
|
||||
testWidgets('enable sync', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.initializeAppFlowy(cloudType: AuthenticatorType.supabase);
|
||||
await tester.tapGoogleLoginInButton();
|
||||
|
||||
// Open the setting page and sign out
|
||||
await tester.openSettings();
|
||||
await tester.openSettingsPage(SettingsPage.syncSetting);
|
||||
await tester.openSettingsPage(SettingsPage.cloud);
|
||||
|
||||
// the switch should be on by default
|
||||
tester.assertEnableSyncSwitchValue(true);
|
||||
await tester.toggleEnableSync();
|
||||
tester.assertSupabaseEnableSyncSwitchValue(true);
|
||||
await tester.toggleEnableSync(SupabaseEnableSync);
|
||||
|
||||
// the switch should be off
|
||||
tester.assertEnableSyncSwitchValue(false);
|
||||
tester.assertSupabaseEnableSyncSwitchValue(false);
|
||||
|
||||
// the switch should be on after toggling
|
||||
await tester.toggleEnableSync();
|
||||
tester.assertEnableSyncSwitchValue(true);
|
||||
await tester.toggleEnableSync(SupabaseEnableSync);
|
||||
tester.assertSupabaseEnableSyncSwitchValue(true);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/plugins/database_view/board/presentation/widgets/board_column_header.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/card/container/card_container.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
||||
import 'package:appflowy_board/appflowy_board.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||
@@ -15,8 +16,8 @@ const defaultLastCardName = 'Card 3';
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('board add row test', () {
|
||||
testWidgets('Add card from header', (tester) async {
|
||||
group('board add row test:', () {
|
||||
testWidgets('from header', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
@@ -45,7 +46,7 @@ void main() {
|
||||
const newCardName = 'Card 4';
|
||||
await tester.enterText(
|
||||
find.descendant(
|
||||
of: find.byType(IntrinsicHeight),
|
||||
of: find.byType(RowCardContainer),
|
||||
matching: find.byType(TextField),
|
||||
),
|
||||
newCardName,
|
||||
@@ -59,7 +60,7 @@ void main() {
|
||||
expect(firstCardText.text, newCardName);
|
||||
});
|
||||
|
||||
testWidgets('Add card from footer', (tester) async {
|
||||
testWidgets('from footer', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
@@ -87,7 +88,7 @@ void main() {
|
||||
const newCardName = 'Card 4';
|
||||
await tester.enterText(
|
||||
find.descendant(
|
||||
of: find.byType(IntrinsicHeight),
|
||||
of: find.byType(RowCardContainer),
|
||||
matching: find.byType(TextField),
|
||||
),
|
||||
newCardName,
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import 'package:appflowy/plugins/database_view/widgets/row/cells/select_option_cell/extension.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/row/row_property.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
import 'package:appflowy_board/appflowy_board.dart';
|
||||
|
||||
import '../util/util.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('board group test', () {
|
||||
testWidgets('move row to another group', (tester) async {
|
||||
const card1Name = 'Card 1';
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
await tester.createNewPageWithName(layout: ViewLayoutPB.Board);
|
||||
final card1 = find.ancestor(
|
||||
of: find.findTextInFlowyText(card1Name),
|
||||
matching: find.byType(AppFlowyGroupCard),
|
||||
);
|
||||
final doingGroup = find.findTextInFlowyText('Doing');
|
||||
final doingGroupCenter = tester.getCenter(doingGroup);
|
||||
final card1Center = tester.getCenter(card1);
|
||||
|
||||
await tester.timedDrag(
|
||||
card1,
|
||||
doingGroupCenter.translate(-card1Center.dx, -card1Center.dy),
|
||||
const Duration(seconds: 1),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(card1);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final card1StatusFinder = find.descendant(
|
||||
of: find.byType(RowPropertyList),
|
||||
matching: find.descendant(
|
||||
of: find.byType(SelectOptionTag),
|
||||
matching: find.byType(FlowyText),
|
||||
),
|
||||
);
|
||||
expect(card1StatusFinder, findsNWidgets(1));
|
||||
final card1StatusText = tester.widget<FlowyText>(card1StatusFinder).text;
|
||||
expect(card1StatusText, 'Doing');
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -3,15 +3,17 @@ import 'package:appflowy/plugins/database_view/board/presentation/widgets/board_
|
||||
import 'package:appflowy/plugins/database_view/board/presentation/widgets/board_hidden_groups.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import '../util/database_test_op.dart';
|
||||
import '../util/util.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('board hide groups test', () {
|
||||
group('board group options:', () {
|
||||
testWidgets('expand/collapse hidden groups', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
@@ -76,6 +78,47 @@ void main() {
|
||||
expect(shownGroups, 4);
|
||||
});
|
||||
});
|
||||
|
||||
testWidgets('delete a group', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
await tester.createNewPageWithName(layout: ViewLayoutPB.Board);
|
||||
|
||||
expect(tester.widgetList(find.byType(BoardColumnHeader)).length, 4);
|
||||
|
||||
// tap group option button for the first group. Delete shouldn't show up
|
||||
await tester.tapButton(
|
||||
find
|
||||
.descendant(
|
||||
of: find.byType(BoardColumnHeader),
|
||||
matching: find.byFlowySvg(FlowySvgs.details_horizontal_s),
|
||||
)
|
||||
.first,
|
||||
);
|
||||
expect(find.byFlowySvg(FlowySvgs.delete_s), findsNothing);
|
||||
|
||||
// dismiss the popup
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.escape);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// tap group option button for the first group. Delete should show up
|
||||
await tester.tapButton(
|
||||
find
|
||||
.descendant(
|
||||
of: find.byType(BoardColumnHeader),
|
||||
matching: find.byFlowySvg(FlowySvgs.details_horizontal_s),
|
||||
)
|
||||
.at(1),
|
||||
);
|
||||
expect(find.byFlowySvg(FlowySvgs.delete_s), findsOneWidget);
|
||||
|
||||
// Tap the delete button and confirm
|
||||
await tester.tapButton(find.byFlowySvg(FlowySvgs.delete_s));
|
||||
await tester.tapDialogOkButton();
|
||||
|
||||
// Expect number of groups to decrease by one
|
||||
expect(tester.widgetList(find.byType(BoardColumnHeader)).length, 3);
|
||||
});
|
||||
}
|
||||
|
||||
extension FlowySvgFinder on CommonFinders {
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import 'board_row_test.dart' as board_row_test;
|
||||
import 'board_add_row_test.dart' as board_add_row_test;
|
||||
import 'board_group_test.dart' as board_group_test;
|
||||
|
||||
void startTesting() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
@@ -9,4 +10,5 @@ void startTesting() {
|
||||
// Board integration tests
|
||||
board_row_test.main();
|
||||
board_add_row_test.main();
|
||||
board_group_test.main();
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import 'auth/appflowy_cloud_auth_test.dart' as appflowy_cloud_auth_test;
|
||||
import 'empty_test.dart' as first_test;
|
||||
|
||||
Future<void> main() async {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
// This test must be run first, otherwise the CI will fail.
|
||||
first_test.main();
|
||||
|
||||
appflowy_cloud_auth_test.main();
|
||||
}
|
||||
@@ -3,8 +3,8 @@ import 'package:appflowy_backend/protobuf/flowy-folder2/view.pbenum.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import 'util/database_test_op.dart';
|
||||
import 'util/util.dart';
|
||||
import '../util/database_test_op.dart';
|
||||
import '../util/util.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
@@ -74,10 +74,10 @@ void main() {
|
||||
await tester.scrollToToday();
|
||||
|
||||
// Hover over today's calendar cell
|
||||
await tester.hoverOnTodayCalendarCell();
|
||||
|
||||
// Tap on create new event button
|
||||
await tester.tapAddCalendarEventButton();
|
||||
await tester.hoverOnTodayCalendarCell(
|
||||
// Tap on create new event button
|
||||
onHover: () async => await tester.tapAddCalendarEventButton(),
|
||||
);
|
||||
|
||||
// Make sure that the event editor popup is shown
|
||||
tester.assertEventEditorOpen();
|
||||
@@ -90,15 +90,9 @@ void main() {
|
||||
// Double click on today's calendar cell to create a new event
|
||||
await tester.doubleClickCalendarCell(DateTime.now());
|
||||
|
||||
// Make sure that the event editor popup is shown
|
||||
tester.assertEventEditorOpen();
|
||||
|
||||
// Make sure that the event is inserted in the cell
|
||||
tester.assertNumberOfEventsInCalendar(2);
|
||||
|
||||
// Dismiss the event editor popup
|
||||
await tester.dismissEventEditor();
|
||||
|
||||
// Click on the event
|
||||
await tester.openCalendarEvent(index: 0);
|
||||
tester.assertEventEditorOpen();
|
||||
@@ -112,7 +106,7 @@ void main() {
|
||||
tester.assertNumberOfEventsOnSpecificDay(2, DateTime.now());
|
||||
|
||||
// Click on the event
|
||||
await tester.openCalendarEvent(index: 1);
|
||||
await tester.openCalendarEvent(index: 0);
|
||||
tester.assertEventEditorOpen();
|
||||
|
||||
// Click on the open icon
|
||||
@@ -137,7 +131,7 @@ void main() {
|
||||
tester.assertNumberOfEventsOnSpecificDay(2, DateTime.now());
|
||||
|
||||
// Delete event from row detail page
|
||||
await tester.openCalendarEvent(index: 1);
|
||||
await tester.openCalendarEvent(index: 0);
|
||||
await tester.openEventToRowDetailPage();
|
||||
tester.assertRowDetailPageOpened();
|
||||
|
||||
@@ -163,7 +157,7 @@ void main() {
|
||||
await tester.dismissEventEditor();
|
||||
|
||||
// Drag and drop the event onto the next week, same day
|
||||
await tester.dragDropRescheduleCalendarEvent(firstOfThisMonth);
|
||||
await tester.dragDropRescheduleCalendarEvent();
|
||||
|
||||
// Make sure that the event has been rescheduled to the new date
|
||||
final sameDayNextWeek = firstOfThisMonth.add(const Duration(days: 7));
|
||||
@@ -4,14 +4,14 @@ import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
import 'util/database_test_op.dart';
|
||||
import 'util/util.dart';
|
||||
import '../util/database_test_op.dart';
|
||||
import '../util/util.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('grid cell', () {
|
||||
testWidgets('edit text cell', (tester) async {
|
||||
group('edit grid cell:', () {
|
||||
testWidgets('text', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
@@ -32,9 +32,9 @@ void main() {
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
|
||||
// Makesure the text cells are filled with the right content when there are
|
||||
// Make sure the text cells are filled with the right content when there are
|
||||
// multiple text cell
|
||||
testWidgets('edit multiple text cells', (tester) async {
|
||||
testWidgets('multiple text cells', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
await tester.createNewPageWithName(
|
||||
@@ -73,7 +73,7 @@ void main() {
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
|
||||
testWidgets('edit number cell', (tester) async {
|
||||
testWidgets('number', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
@@ -131,7 +131,7 @@ void main() {
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
|
||||
testWidgets('edit checkbox cell', (tester) async {
|
||||
testWidgets('checkbox', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
@@ -149,7 +149,7 @@ void main() {
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
|
||||
testWidgets('edit create time cell', (tester) async {
|
||||
testWidgets('created time', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
@@ -167,7 +167,7 @@ void main() {
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
|
||||
testWidgets('edit last time cell', (tester) async {
|
||||
testWidgets('last modified time', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
@@ -185,7 +185,7 @@ void main() {
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
|
||||
testWidgets('edit date time cell', (tester) async {
|
||||
testWidgets('date time', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
@@ -275,7 +275,7 @@ void main() {
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
|
||||
testWidgets('edit single select cell', (tester) async {
|
||||
testWidgets('single select', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
@@ -347,7 +347,7 @@ void main() {
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
|
||||
testWidgets('edit multi select cell', (tester) async {
|
||||
testWidgets('multi select', (tester) async {
|
||||
final tags = [
|
||||
'tag 1',
|
||||
'tag 2',
|
||||
@@ -436,107 +436,107 @@ void main() {
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
});
|
||||
|
||||
testWidgets('edit checklist cell', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
testWidgets('checklist', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
await tester.createNewPageWithName(layout: ViewLayoutPB.Grid);
|
||||
await tester.createNewPageWithName(layout: ViewLayoutPB.Grid);
|
||||
|
||||
const fieldType = FieldType.Checklist;
|
||||
await tester.createField(fieldType, fieldType.name);
|
||||
const fieldType = FieldType.Checklist;
|
||||
await tester.createField(fieldType, fieldType.name);
|
||||
|
||||
// assert that there is no progress bar in the grid
|
||||
tester.assertChecklistCellInGrid(rowIndex: 0, percent: null);
|
||||
// assert that there is no progress bar in the grid
|
||||
tester.assertChecklistCellInGrid(rowIndex: 0, percent: null);
|
||||
|
||||
// tap on the first checklist cell
|
||||
await tester.tapChecklistCellInGrid(rowIndex: 0);
|
||||
// tap on the first checklist cell
|
||||
await tester.tapChecklistCellInGrid(rowIndex: 0);
|
||||
|
||||
// assert that the checklist editor is shown
|
||||
tester.assertChecklistEditorVisible(visible: true);
|
||||
// assert that the checklist editor is shown
|
||||
tester.assertChecklistEditorVisible(visible: true);
|
||||
|
||||
// create a new task with enter
|
||||
await tester.createNewChecklistTask(name: "task 0", enter: true);
|
||||
// create a new task with enter
|
||||
await tester.createNewChecklistTask(name: "task 0", enter: true);
|
||||
|
||||
// assert that the task is displayed
|
||||
tester.assertChecklistTaskInEditor(
|
||||
index: 0,
|
||||
name: "task 0",
|
||||
isChecked: false,
|
||||
);
|
||||
// assert that the task is displayed
|
||||
tester.assertChecklistTaskInEditor(
|
||||
index: 0,
|
||||
name: "task 0",
|
||||
isChecked: false,
|
||||
);
|
||||
|
||||
// update the task's name
|
||||
await tester.renameChecklistTask(index: 0, name: "task 1");
|
||||
// update the task's name
|
||||
await tester.renameChecklistTask(index: 0, name: "task 1");
|
||||
|
||||
// assert that the task's name is updated
|
||||
tester.assertChecklistTaskInEditor(
|
||||
index: 0,
|
||||
name: "task 1",
|
||||
isChecked: false,
|
||||
);
|
||||
// assert that the task's name is updated
|
||||
tester.assertChecklistTaskInEditor(
|
||||
index: 0,
|
||||
name: "task 1",
|
||||
isChecked: false,
|
||||
);
|
||||
|
||||
// dismiss new task editor
|
||||
await tester.dismissCellEditor();
|
||||
// dismiss new task editor
|
||||
await tester.dismissCellEditor();
|
||||
|
||||
// dismiss checklist cell editor
|
||||
await tester.dismissCellEditor();
|
||||
// dismiss checklist cell editor
|
||||
await tester.dismissCellEditor();
|
||||
|
||||
// assert that progress bar is shown in grid at 0%
|
||||
tester.assertChecklistCellInGrid(rowIndex: 0, percent: 0);
|
||||
// assert that progress bar is shown in grid at 0%
|
||||
tester.assertChecklistCellInGrid(rowIndex: 0, percent: 0);
|
||||
|
||||
// start editing the first checklist cell again
|
||||
await tester.tapChecklistCellInGrid(rowIndex: 0);
|
||||
// start editing the first checklist cell again
|
||||
await tester.tapChecklistCellInGrid(rowIndex: 0);
|
||||
|
||||
// create another task with the create button
|
||||
await tester.createNewChecklistTask(name: "task 2", button: true);
|
||||
// create another task with the create button
|
||||
await tester.createNewChecklistTask(name: "task 2", button: true);
|
||||
|
||||
// assert that the task was inserted
|
||||
tester.assertChecklistTaskInEditor(
|
||||
index: 1,
|
||||
name: "task 2",
|
||||
isChecked: false,
|
||||
);
|
||||
// assert that the task was inserted
|
||||
tester.assertChecklistTaskInEditor(
|
||||
index: 1,
|
||||
name: "task 2",
|
||||
isChecked: false,
|
||||
);
|
||||
|
||||
// mark it as complete
|
||||
await tester.checkChecklistTask(index: 1);
|
||||
// mark it as complete
|
||||
await tester.checkChecklistTask(index: 1);
|
||||
|
||||
// assert that the task was checked in the editor
|
||||
tester.assertChecklistTaskInEditor(
|
||||
index: 1,
|
||||
name: "task 2",
|
||||
isChecked: true,
|
||||
);
|
||||
// assert that the task was checked in the editor
|
||||
tester.assertChecklistTaskInEditor(
|
||||
index: 1,
|
||||
name: "task 2",
|
||||
isChecked: true,
|
||||
);
|
||||
|
||||
// dismiss checklist editor
|
||||
await tester.dismissCellEditor();
|
||||
await tester.dismissCellEditor();
|
||||
// dismiss checklist editor
|
||||
await tester.dismissCellEditor();
|
||||
await tester.dismissCellEditor();
|
||||
|
||||
// assert that progressbar is shown in grid at 50%
|
||||
tester.assertChecklistCellInGrid(rowIndex: 0, percent: 0.5);
|
||||
// assert that progressbar is shown in grid at 50%
|
||||
tester.assertChecklistCellInGrid(rowIndex: 0, percent: 0.5);
|
||||
|
||||
// re-open the cell editor
|
||||
await tester.tapChecklistCellInGrid(rowIndex: 0);
|
||||
// re-open the cell editor
|
||||
await tester.tapChecklistCellInGrid(rowIndex: 0);
|
||||
|
||||
// hover over first task and delete it
|
||||
await tester.deleteChecklistTask(index: 0);
|
||||
// hover over first task and delete it
|
||||
await tester.deleteChecklistTask(index: 0);
|
||||
|
||||
// dismiss cell editor
|
||||
await tester.dismissCellEditor();
|
||||
// dismiss cell editor
|
||||
await tester.dismissCellEditor();
|
||||
|
||||
// assert that progressbar is shown in grid at 100%
|
||||
tester.assertChecklistCellInGrid(rowIndex: 0, percent: 1);
|
||||
// assert that progressbar is shown in grid at 100%
|
||||
tester.assertChecklistCellInGrid(rowIndex: 0, percent: 1);
|
||||
|
||||
// re-open the cell edior
|
||||
await tester.tapChecklistCellInGrid(rowIndex: 0);
|
||||
// re-open the cell edior
|
||||
await tester.tapChecklistCellInGrid(rowIndex: 0);
|
||||
|
||||
// delete the remaining task
|
||||
await tester.deleteChecklistTask(index: 0);
|
||||
// delete the remaining task
|
||||
await tester.deleteChecklistTask(index: 0);
|
||||
|
||||
// dismiss the cell editor
|
||||
await tester.dismissCellEditor();
|
||||
// dismiss the cell editor
|
||||
await tester.dismissCellEditor();
|
||||
|
||||
// check that the progress bar is not viisble
|
||||
tester.assertChecklistCellInGrid(rowIndex: 0, percent: null);
|
||||
// check that the progress bar is not viisble
|
||||
tester.assertChecklistCellInGrid(rowIndex: 0, percent: null);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/grid_page.dart';
|
||||
import 'package:appflowy/plugins/database_view/tar_bar/tar_bar_add_button.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pbenum.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pbenum.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import 'util/database_test_op.dart';
|
||||
import 'util/util.dart';
|
||||
import '../util/database_test_op.dart';
|
||||
import '../util/util.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
@@ -16,7 +16,7 @@ void main() {
|
||||
await tester.tapGoButton();
|
||||
|
||||
await tester.createNewPageWithName(layout: ViewLayoutPB.Grid);
|
||||
await tester.tapCreateLinkedDatabaseViewButton(AddButtonAction.grid);
|
||||
await tester.tapCreateLinkedDatabaseViewButton(DatabaseLayoutPB.Grid);
|
||||
|
||||
// create a field
|
||||
await tester.scrollToRight(find.byType(GridPage));
|
||||
@@ -34,7 +34,7 @@ void main() {
|
||||
await tester.findFieldWithName('New field 1');
|
||||
|
||||
// go back to linked database view, expect field to be hidden
|
||||
await tester.tapTabBarLinkedViewByViewName('grid');
|
||||
await tester.tapTabBarLinkedViewByViewName('Grid');
|
||||
await tester.noFieldWithName('New field 1');
|
||||
|
||||
// use the settings button to show the field
|
||||
@@ -6,13 +6,13 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import 'util/database_test_op.dart';
|
||||
import 'util/util.dart';
|
||||
import '../util/database_test_op.dart';
|
||||
import '../util/util.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('grid page', () {
|
||||
group('grid field editor:', () {
|
||||
testWidgets('rename existing field', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
@@ -98,7 +98,7 @@ void main() {
|
||||
await tester.renameField('New field 1');
|
||||
await tester.dismissFieldEditor();
|
||||
|
||||
// Delete the field
|
||||
// duplicate the field
|
||||
await tester.tapGridFieldWithName('New field 1');
|
||||
await tester.tapDuplicatePropertyButton();
|
||||
|
||||
@@ -106,7 +106,30 @@ void main() {
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
|
||||
testWidgets('create checklist field ', (tester) async {
|
||||
testWidgets('insert field on either side of a field', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
await tester.createNewPageWithName(layout: ViewLayoutPB.Grid);
|
||||
|
||||
await tester.scrollToRight(find.byType(GridPage));
|
||||
|
||||
// insert new field to the right
|
||||
await tester.tapGridFieldWithName('Type');
|
||||
await tester.tapInsertFieldButton(left: false, name: 'Right');
|
||||
await tester.dismissFieldEditor();
|
||||
await tester.findFieldWithName('Right');
|
||||
|
||||
// insert new field to the right
|
||||
await tester.tapGridFieldWithName('Type');
|
||||
await tester.tapInsertFieldButton(left: true, name: "Left");
|
||||
await tester.dismissFieldEditor();
|
||||
await tester.findFieldWithName('Left');
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
|
||||
testWidgets('create checklist field', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
@@ -4,7 +4,7 @@ import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import 'util/database_test_op.dart';
|
||||
import '../util/database_test_op.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
@@ -7,9 +7,9 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import 'util/database_test_op.dart';
|
||||
import 'util/emoji.dart';
|
||||
import 'util/util.dart';
|
||||
import '../util/database_test_op.dart';
|
||||
import '../util/emoji.dart';
|
||||
import '../util/util.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
@@ -123,7 +123,6 @@ void main() {
|
||||
await tester.tapTypeOptionButton();
|
||||
|
||||
await tester.selectFieldType(fieldType);
|
||||
await tester.dismissFieldEditor();
|
||||
|
||||
// After update the field type, the cells should be updated
|
||||
await tester.findCellByFieldType(fieldType);
|
||||
@@ -2,8 +2,8 @@ import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import 'util/database_test_op.dart';
|
||||
import 'util/util.dart';
|
||||
import '../util/database_test_op.dart';
|
||||
import '../util/util.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
@@ -3,8 +3,8 @@ import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import 'util/database_test_op.dart';
|
||||
import 'util/util.dart';
|
||||
import '../util/database_test_op.dart';
|
||||
import '../util/util.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
@@ -2,7 +2,7 @@ import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import 'util/database_test_op.dart';
|
||||
import '../util/database_test_op.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
@@ -34,7 +34,7 @@ void main() {
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
false,
|
||||
];
|
||||
for (final (index, content) in checkboxCells.indexed) {
|
||||
await tester.assertCheckboxCell(
|
||||
@@ -54,7 +54,7 @@ void main() {
|
||||
'10',
|
||||
'11',
|
||||
'12',
|
||||
''
|
||||
'',
|
||||
];
|
||||
for (final (index, content) in numberCells.indexed) {
|
||||
await tester.assertCellContent(
|
||||
@@ -152,7 +152,7 @@ void main() {
|
||||
'Jun 16, 2023',
|
||||
'',
|
||||
'',
|
||||
''
|
||||
'',
|
||||
];
|
||||
for (final (index, content) in dateCells.indexed) {
|
||||
await tester.assertDateCellInGrid(
|
||||
@@ -2,7 +2,7 @@ import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import 'util/database_test_op.dart';
|
||||
import '../util/database_test_op.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
@@ -1,11 +1,10 @@
|
||||
import 'package:appflowy/plugins/database_view/tar_bar/tar_bar_add_button.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pbenum.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import 'util/database_test_op.dart';
|
||||
import 'util/util.dart';
|
||||
import '../util/database_test_op.dart';
|
||||
import '../util/util.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
@@ -18,15 +17,15 @@ void main() {
|
||||
await tester.createNewPageWithName(layout: ViewLayoutPB.Grid);
|
||||
|
||||
// Create board view
|
||||
await tester.tapCreateLinkedDatabaseViewButton(AddButtonAction.board);
|
||||
await tester.tapCreateLinkedDatabaseViewButton(DatabaseLayoutPB.Board);
|
||||
tester.assertCurrentDatabaseTagIs(DatabaseLayoutPB.Board);
|
||||
|
||||
// Create grid view
|
||||
await tester.tapCreateLinkedDatabaseViewButton(AddButtonAction.grid);
|
||||
await tester.tapCreateLinkedDatabaseViewButton(DatabaseLayoutPB.Grid);
|
||||
tester.assertCurrentDatabaseTagIs(DatabaseLayoutPB.Grid);
|
||||
|
||||
// Create calendar view
|
||||
await tester.tapCreateLinkedDatabaseViewButton(AddButtonAction.calendar);
|
||||
await tester.tapCreateLinkedDatabaseViewButton(DatabaseLayoutPB.Calendar);
|
||||
tester.assertCurrentDatabaseTagIs(DatabaseLayoutPB.Calendar);
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
@@ -39,7 +38,7 @@ void main() {
|
||||
await tester.createNewPageWithName(layout: ViewLayoutPB.Grid);
|
||||
|
||||
// Create board view
|
||||
await tester.tapCreateLinkedDatabaseViewButton(AddButtonAction.board);
|
||||
await tester.tapCreateLinkedDatabaseViewButton(DatabaseLayoutPB.Board);
|
||||
tester.assertCurrentDatabaseTagIs(DatabaseLayoutPB.Board);
|
||||
|
||||
// rename board view
|
||||
@@ -64,7 +63,7 @@ void main() {
|
||||
await tester.createNewPageWithName(layout: ViewLayoutPB.Grid);
|
||||
|
||||
// Create board view
|
||||
await tester.tapCreateLinkedDatabaseViewButton(AddButtonAction.board);
|
||||
await tester.tapCreateLinkedDatabaseViewButton(DatabaseLayoutPB.Board);
|
||||
tester.assertCurrentDatabaseTagIs(DatabaseLayoutPB.Board);
|
||||
|
||||
// delete the board
|
||||
@@ -219,7 +219,7 @@ void main() {
|
||||
expect(node.delta!.toJson(), [
|
||||
{
|
||||
'insert': text,
|
||||
'attributes': {'href': url}
|
||||
'attributes': {'href': url},
|
||||
}
|
||||
]);
|
||||
},
|
||||
|
||||
@@ -2,8 +2,8 @@ import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/document_header_node_widget.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:emoji_mart/emoji_mart.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_emoji_mart/flutter_emoji_mart.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ void main() {
|
||||
|
||||
// create a new document
|
||||
await tester.createNewPageWithName(
|
||||
name: LocaleKeys.document_plugins_createInlineMathEquation.tr(),
|
||||
name: 'math equation',
|
||||
layout: ViewLayoutPB.Document,
|
||||
);
|
||||
|
||||
@@ -67,7 +67,7 @@ void main() {
|
||||
|
||||
// create a new document
|
||||
await tester.createNewPageWithName(
|
||||
name: LocaleKeys.document_plugins_createInlineMathEquation.tr(),
|
||||
name: 'math equation',
|
||||
layout: ViewLayoutPB.Document,
|
||||
);
|
||||
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/openai_client.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:appflowy_editor/src/render/toolbar/toolbar_widget.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 'util/mock/mock_openai_repository.dart';
|
||||
import 'util/util.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:appflowy_editor/src/render/toolbar/toolbar_widget.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
@@ -1,20 +1,19 @@
|
||||
import 'package:appflowy/env/env.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import 'appearance_settings_test.dart' as appearance_test_runner;
|
||||
import 'auth/auth_test.dart' as auth_test_runner;
|
||||
import 'board/board_test_runner.dart' as board_test_runner;
|
||||
import 'database_calendar_test.dart' as database_calendar_test;
|
||||
import 'database_cell_test.dart' as database_cell_test;
|
||||
import 'database_field_settings_test.dart' as database_field_settings_test;
|
||||
import 'database_field_test.dart' as database_field_test;
|
||||
import 'database_filter_test.dart' as database_filter_test;
|
||||
import 'database_row_page_test.dart' as database_row_page_test;
|
||||
import 'database_row_test.dart' as database_row_test;
|
||||
import 'database_setting_test.dart' as database_setting_test;
|
||||
import 'database_share_test.dart' as database_share_test;
|
||||
import 'database_sort_test.dart' as database_sort_test;
|
||||
import 'database_view_test.dart' as database_view_test;
|
||||
import 'database/database_calendar_test.dart' as database_calendar_test;
|
||||
import 'database/database_cell_test.dart' as database_cell_test;
|
||||
import 'database/database_field_settings_test.dart'
|
||||
as database_field_settings_test;
|
||||
import 'database/database_field_test.dart' as database_field_test;
|
||||
import 'database/database_filter_test.dart' as database_filter_test;
|
||||
import 'database/database_row_page_test.dart' as database_row_page_test;
|
||||
import 'database/database_row_test.dart' as database_row_test;
|
||||
import 'database/database_setting_test.dart' as database_setting_test;
|
||||
import 'database/database_share_test.dart' as database_share_test;
|
||||
import 'database/database_sort_test.dart' as database_sort_test;
|
||||
import 'database/database_view_test.dart' as database_view_test;
|
||||
import 'document/document_test_runner.dart' as document_test_runner;
|
||||
import 'empty_test.dart' as first_test;
|
||||
import 'hotkeys_test.dart' as hotkeys_test;
|
||||
@@ -24,6 +23,7 @@ import 'share_markdown_test.dart' as share_markdown_test;
|
||||
import 'sidebar/sidebar_test_runner.dart' as sidebar_test_runner;
|
||||
import 'switch_folder_test.dart' as switch_folder_test;
|
||||
import 'tabs_test.dart' as tabs_test;
|
||||
// import 'auth/supabase_auth_test.dart' as supabase_auth_test_runner;
|
||||
|
||||
/// The main task runner for all integration tests in AppFlowy.
|
||||
///
|
||||
@@ -32,7 +32,7 @@ import 'tabs_test.dart' as tabs_test;
|
||||
/// If flutter/flutter#101031 is resolved, this file can be removed completely.
|
||||
/// Once removed, the integration_test.yaml must be updated to exclude this as
|
||||
/// as the test target.
|
||||
void main() {
|
||||
Future<void> main() async {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
// This test must be run first, otherwise the CI will fail.
|
||||
@@ -76,10 +76,6 @@ void main() {
|
||||
// User settings
|
||||
settings_test_runner.main();
|
||||
|
||||
if (isCloudEnabled) {
|
||||
auth_test_runner.main();
|
||||
}
|
||||
|
||||
// board_test.main();
|
||||
// empty_document_test.main();
|
||||
// smart_menu_test.main();
|
||||
|
||||
@@ -3,6 +3,8 @@ import 'package:appflowy/workspace/presentation/settings/widgets/settings_user_v
|
||||
import 'package:appflowy/workspace/presentation/widgets/user_avatar.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import '../util/emoji.dart';
|
||||
import '../util/util.dart';
|
||||
|
||||
void main() {
|
||||
@@ -28,27 +30,12 @@ void main() {
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Select first option that isn't default
|
||||
await tester.tap(find.byType(IconOption).first);
|
||||
await tester.tapEmoji('😁');
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
UserAvatar userAvatar = tester.widget(userAvatarFinder) as UserAvatar;
|
||||
expect(userAvatar.iconUrl, isNotEmpty);
|
||||
|
||||
// Open icon picker dialog again
|
||||
await tester.tap(userAvatarFinder);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Tap the default option
|
||||
await tester.tap(
|
||||
find.descendant(
|
||||
of: find.byType(IconGallery),
|
||||
matching: find.byType(UserAvatar),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
userAvatar = tester.widget(userAvatarFinder) as UserAvatar;
|
||||
expect(userAvatar.iconUrl, isEmpty);
|
||||
final UserAvatar userAvatar =
|
||||
tester.widget(userAvatarFinder) as UserAvatar;
|
||||
expect(userAvatar.iconUrl, '😁');
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/widgets.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/sync_setting_view.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/setting_supabase_cloud.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
@@ -34,10 +35,20 @@ extension AppFlowyAuthTest on WidgetTester {
|
||||
);
|
||||
}
|
||||
|
||||
void assertEnableSyncSwitchValue(bool value) {
|
||||
void assertSupabaseEnableSyncSwitchValue(bool value) {
|
||||
assertSwitchValue(
|
||||
find.descendant(
|
||||
of: find.byType(EnableSync),
|
||||
of: find.byType(SupabaseEnableSync),
|
||||
matching: find.byWidgetPredicate((widget) => widget is Switch),
|
||||
),
|
||||
value,
|
||||
);
|
||||
}
|
||||
|
||||
void assertAppFlowyCloudEnableSyncSwitchValue(bool value) {
|
||||
assertSwitchValue(
|
||||
find.descendant(
|
||||
of: find.byType(AppFlowyCloudEnableSync),
|
||||
matching: find.byWidgetPredicate((widget) => widget is Switch),
|
||||
),
|
||||
value,
|
||||
@@ -53,9 +64,9 @@ extension AppFlowyAuthTest on WidgetTester {
|
||||
await tapButton(finder);
|
||||
}
|
||||
|
||||
Future<void> toggleEnableSync() async {
|
||||
Future<void> toggleEnableSync(Type syncButton) async {
|
||||
final finder = find.descendant(
|
||||
of: find.byType(EnableSync),
|
||||
of: find.byType(syncButton),
|
||||
matching: find.byWidgetPredicate((widget) => widget is Switch),
|
||||
);
|
||||
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:appflowy/env/env.dart';
|
||||
import 'package:appflowy/env/cloud_env.dart';
|
||||
import 'package:appflowy/env/cloud_env_test.dart';
|
||||
import 'package:appflowy/startup/entry_point.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/user/application/auth/af_cloud_mock_auth_service.dart';
|
||||
import 'package:appflowy/user/application/auth/auth_service.dart';
|
||||
import 'package:appflowy/user/application/auth/supabase_mock_auth_service.dart';
|
||||
import 'package:appflowy/user/presentation/presentation.dart';
|
||||
import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/widgets.dart';
|
||||
import 'package:appflowy/workspace/application/settings/prelude.dart';
|
||||
import 'package:dartz/dartz.dart';
|
||||
import 'package:flowy_infra/uuid.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
@@ -29,6 +34,8 @@ extension AppFlowyTestBase on WidgetTester {
|
||||
// use to append after the application data directory
|
||||
String? pathExtension,
|
||||
Size windowsSize = const Size(1600, 1200),
|
||||
AuthenticatorType? cloudType,
|
||||
String? userEmail,
|
||||
}) async {
|
||||
binding.setSurfaceSize(windowsSize);
|
||||
|
||||
@@ -38,13 +45,54 @@ extension AppFlowyTestBase on WidgetTester {
|
||||
);
|
||||
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
await FlowyRunner.run(
|
||||
FlowyApp(),
|
||||
IntegrationMode.integrationTest,
|
||||
rustEnvsBuilder: () {
|
||||
final rustEnvs = <String, String>{};
|
||||
if (cloudType != null) {
|
||||
switch (cloudType) {
|
||||
case AuthenticatorType.local:
|
||||
break;
|
||||
case AuthenticatorType.supabase:
|
||||
break;
|
||||
case AuthenticatorType.appflowyCloud:
|
||||
rustEnvs["GOTRUE_ADMIN_EMAIL"] = "admin@example.com";
|
||||
rustEnvs["GOTRUE_ADMIN_PASSWORD"] = "password";
|
||||
break;
|
||||
}
|
||||
}
|
||||
return rustEnvs;
|
||||
},
|
||||
didInitGetItCallback: () {
|
||||
return Future(
|
||||
() async {
|
||||
if (cloudType != null) {
|
||||
switch (cloudType) {
|
||||
case AuthenticatorType.local:
|
||||
break;
|
||||
case AuthenticatorType.supabase:
|
||||
await useSupabaseCloud();
|
||||
getIt.unregister<AuthService>();
|
||||
getIt.registerFactory<AuthService>(
|
||||
() => SupabaseMockAuthService(),
|
||||
);
|
||||
break;
|
||||
case AuthenticatorType.appflowyCloud:
|
||||
await useAppFlowyCloud();
|
||||
getIt.unregister<AuthService>();
|
||||
getIt.registerFactory<AuthService>(
|
||||
() => AppFlowyCloudMockAuthService(email: userEmail),
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
await waitUntilSignInPageShow();
|
||||
|
||||
return FlowyTestContext(
|
||||
applicationDataDirectory: directory,
|
||||
);
|
||||
@@ -83,7 +131,7 @@ extension AppFlowyTestBase on WidgetTester {
|
||||
}
|
||||
|
||||
Future<void> waitUntilSignInPageShow() async {
|
||||
if (isCloudEnabled) {
|
||||
if (isAuthEnabled) {
|
||||
final finder = find.byType(SignInAnonymousButton);
|
||||
await pumpUntilFound(finder);
|
||||
expect(finder, findsOneWidget);
|
||||
@@ -207,3 +255,16 @@ extension AppFlowyFinderTestBase on CommonFinders {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> useSupabaseCloud() async {
|
||||
await setAuthenticatorType(AuthenticatorType.supabase);
|
||||
await setSupbaseServer(
|
||||
Some(TestEnv.supabaseUrl),
|
||||
Some(TestEnv.supabaseAnonKey),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> useAppFlowyCloud() async {
|
||||
await setAuthenticatorType(AuthenticatorType.appflowyCloud);
|
||||
// await setAppFlowyCloudUrl(Some(TestEnv.afCloudUrl));
|
||||
}
|
||||
@@ -303,7 +303,7 @@ extension CommonOperations on WidgetTester {
|
||||
KVKeys.showRenameDialogWhenCreatingNewFile,
|
||||
(value) => bool.parse(value),
|
||||
);
|
||||
final showRenameDialog = settingsOrFailure.fold((l) => false, (r) => r);
|
||||
final showRenameDialog = settingsOrFailure.fold(() => false, (r) => r);
|
||||
if (showRenameDialog) {
|
||||
await tapOKButton();
|
||||
}
|
||||
|
||||
@@ -21,7 +21,6 @@ import 'package:appflowy/plugins/database_view/grid/presentation/widgets/filter/
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/filter/filter_menu_item.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/footer/grid_footer.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_cell.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_cell_action_sheet.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_editor.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_type_extension.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_type_list.dart';
|
||||
@@ -34,10 +33,10 @@ import 'package:appflowy/plugins/database_view/grid/presentation/widgets/sort/or
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/sort/sort_editor.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/sort/sort_menu.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/toolbar/filter_button.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/toolbar/grid_layout.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/setting/database_layout_selector.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/toolbar/sort_button.dart';
|
||||
import 'package:appflowy/plugins/database_view/tar_bar/tab_bar_header.dart';
|
||||
import 'package:appflowy/plugins/database_view/tar_bar/tar_bar_add_button.dart';
|
||||
import 'package:appflowy/plugins/database_view/tab_bar/desktop/tab_bar_add_button.dart';
|
||||
import 'package:appflowy/plugins/database_view/tab_bar/desktop/tab_bar_header.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/database_layout_ext.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/row/accessory/cell_accessory.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/row/cells/cells.dart';
|
||||
@@ -52,6 +51,8 @@ import 'package:appflowy/plugins/database_view/widgets/row/row_banner.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/row/row_detail.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/row/row_document.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/row/row_property.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/setting/database_setting_action.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/setting/database_settings_list.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/setting/setting_button.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/setting/setting_property_list.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
||||
@@ -222,7 +223,9 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
||||
final finder = find.descendant(
|
||||
of: findCell,
|
||||
matching: find.byWidgetPredicate(
|
||||
(widget) => widget is SelectOptionTag && widget.name == content,
|
||||
(widget) =>
|
||||
widget is SelectOptionTag &&
|
||||
(widget.name == content || widget.option?.name == content),
|
||||
),
|
||||
);
|
||||
expect(finder, findsOneWidget);
|
||||
@@ -239,7 +242,9 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
||||
final finder = find.descendant(
|
||||
of: findCell,
|
||||
matching: find.byWidgetPredicate(
|
||||
(widget) => widget is SelectOptionTag && widget.name == content,
|
||||
(widget) =>
|
||||
widget is SelectOptionTag &&
|
||||
(widget.name == content || widget.option?.name == content),
|
||||
),
|
||||
);
|
||||
expect(finder, findsOneWidget);
|
||||
@@ -287,13 +292,8 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
||||
skipOffstage: false,
|
||||
);
|
||||
|
||||
final dateCellText = find.descendant(
|
||||
of: findCell,
|
||||
matching: find.byType(GridDateCellText),
|
||||
);
|
||||
|
||||
final text = find.descendant(
|
||||
of: dateCellText,
|
||||
of: findCell,
|
||||
matching: find.byWidgetPredicate(
|
||||
(widget) {
|
||||
if (widget is FlowyText) {
|
||||
@@ -445,7 +445,9 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
||||
}) async {
|
||||
final findRow = find.byType(GridRow);
|
||||
final option = find.byWidgetPredicate(
|
||||
(widget) => widget is SelectOptionTag && widget.name == name,
|
||||
(widget) =>
|
||||
widget is SelectOptionTag &&
|
||||
(widget.name == name || widget.option?.name == name),
|
||||
);
|
||||
|
||||
final cell = find.descendant(
|
||||
@@ -688,7 +690,10 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
||||
}
|
||||
|
||||
Future<void> tapDeletePropertyInFieldEditor() async {
|
||||
final deleteButton = find.byType(DeleteFieldButton);
|
||||
final deleteButton = find.byWidgetPredicate(
|
||||
(widget) =>
|
||||
widget is FieldActionCell && widget.action == FieldAction.delete,
|
||||
);
|
||||
await tapButton(deleteButton);
|
||||
|
||||
final confirmButton = find.descendant(
|
||||
@@ -761,17 +766,36 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
||||
await tapButton(field);
|
||||
}
|
||||
|
||||
Future<void> tapInsertFieldButton({
|
||||
required bool left,
|
||||
required String name,
|
||||
}) async {
|
||||
final field = find.byWidgetPredicate(
|
||||
(widget) =>
|
||||
widget is FieldActionCell &&
|
||||
(left && widget.action == FieldAction.insertLeft ||
|
||||
!left && widget.action == FieldAction.insertRight),
|
||||
);
|
||||
await tapButton(field);
|
||||
await renameField(name);
|
||||
}
|
||||
|
||||
/// Should call [tapGridFieldWithName] first.
|
||||
Future<void> tapHidePropertyButton() async {
|
||||
final field = find.byWidgetPredicate(
|
||||
(widget) =>
|
||||
widget is FieldActionCell && widget.action == FieldAction.hide,
|
||||
widget is FieldActionCell &&
|
||||
widget.action == FieldAction.toggleVisibility,
|
||||
);
|
||||
await tapButton(field);
|
||||
}
|
||||
|
||||
Future<void> tapHidePropertyButtonInFieldEditor() async {
|
||||
final button = find.byType(FieldVisibilityToggleButton);
|
||||
final button = find.byWidgetPredicate(
|
||||
(widget) =>
|
||||
widget is FieldActionCell &&
|
||||
widget.action == FieldAction.toggleVisibility,
|
||||
);
|
||||
await tapButton(button);
|
||||
}
|
||||
|
||||
@@ -1131,7 +1155,7 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
||||
|
||||
/// Should call [tapDatabaseSettingButton] first.
|
||||
Future<void> tapViewPropertiesButton() async {
|
||||
final findSettingItem = find.byType(DatabaseSettingListPopover);
|
||||
final findSettingItem = find.byType(DatabaseSettingsList);
|
||||
final findLayoutButton = find.byWidgetPredicate(
|
||||
(widget) =>
|
||||
widget is FlowyText &&
|
||||
@@ -1148,7 +1172,7 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
||||
|
||||
/// Should call [tapDatabaseSettingButton] first.
|
||||
Future<void> tapDatabaseLayoutButton() async {
|
||||
final findSettingItem = find.byType(DatabaseSettingListPopover);
|
||||
final findSettingItem = find.byType(DatabaseSettingsList);
|
||||
final findLayoutButton = find.byWidgetPredicate(
|
||||
(widget) =>
|
||||
widget is FlowyText &&
|
||||
@@ -1164,7 +1188,7 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
||||
}
|
||||
|
||||
Future<void> tapCalendarLayoutSettingButton() async {
|
||||
final findSettingItem = find.byType(DatabaseSettingListPopover);
|
||||
final findSettingItem = find.byType(DatabaseSettingsList);
|
||||
final findLayoutButton = find.byWidgetPredicate(
|
||||
(widget) =>
|
||||
widget is FlowyText &&
|
||||
@@ -1242,12 +1266,14 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
||||
await pumpAndSettle(const Duration(milliseconds: 300));
|
||||
}
|
||||
|
||||
Future<void> hoverOnTodayCalendarCell() async {
|
||||
Future<void> hoverOnTodayCalendarCell({
|
||||
Future<void> Function()? onHover,
|
||||
}) async {
|
||||
final todayCell = find.byWidgetPredicate(
|
||||
(widget) => widget is CalendarDayCard && widget.isToday,
|
||||
);
|
||||
|
||||
await hoverOnWidget(todayCell);
|
||||
await hoverOnWidget(todayCell, onHover: onHover);
|
||||
}
|
||||
|
||||
Future<void> tapAddCalendarEventButton() async {
|
||||
@@ -1355,7 +1381,7 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
||||
await tapButton(button);
|
||||
}
|
||||
|
||||
Future<void> dragDropRescheduleCalendarEvent(DateTime startDate) async {
|
||||
Future<void> dragDropRescheduleCalendarEvent() async {
|
||||
final findEventCard = find.byType(EventCard);
|
||||
await drag(findEventCard.first, const Offset(0, 300));
|
||||
await pumpAndSettle();
|
||||
@@ -1381,13 +1407,15 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
||||
await tapButton(unscheduledEvent);
|
||||
}
|
||||
|
||||
Future<void> tapCreateLinkedDatabaseViewButton(AddButtonAction action) async {
|
||||
Future<void> tapCreateLinkedDatabaseViewButton(
|
||||
DatabaseLayoutPB layoutType,
|
||||
) async {
|
||||
final findAddButton = find.byType(AddDatabaseViewButton);
|
||||
await tapButton(findAddButton);
|
||||
|
||||
final findCreateButton = find.byWidgetPredicate(
|
||||
(widget) =>
|
||||
widget is TarBarAddButtonActionCell && widget.action == action,
|
||||
widget is TabBarAddButtonActionCell && widget.action == layoutType,
|
||||
);
|
||||
await tapButton(findCreateButton);
|
||||
}
|
||||
@@ -1554,7 +1582,7 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
||||
Future<void> selectDatabaseLayoutType(DatabaseLayoutPB layout) async {
|
||||
final findLayoutCell = find.byType(DatabaseViewLayoutCell);
|
||||
final findText = find.byWidgetPredicate(
|
||||
(widget) => widget is FlowyText && widget.text == layout.layoutName(),
|
||||
(widget) => widget is FlowyText && widget.text == layout.layoutName,
|
||||
);
|
||||
|
||||
final button = find.descendant(
|
||||
@@ -1582,7 +1610,8 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
||||
) async {
|
||||
final field = find.byWidgetPredicate(
|
||||
(widget) =>
|
||||
widget is DatabasePropertyCell && widget.fieldInfo.name == fieldName,
|
||||
widget is DesktopDatabasePropertyCell &&
|
||||
widget.fieldInfo.name == fieldName,
|
||||
);
|
||||
final toggleVisibilityButton =
|
||||
find.descendant(of: field, matching: find.byType(FlowyIconButton));
|
||||
|
||||
@@ -12,9 +12,9 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emo
|
||||
import 'package:appflowy/plugins/inline_actions/widgets/inline_actions_handler.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart' hide Log;
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:emoji_mart/emoji_mart.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_emoji_mart/flutter_emoji_mart.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import 'util.dart';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import 'package:emoji_mart/emoji_mart.dart';
|
||||
import 'package:flutter_emoji_mart/flutter_emoji_mart.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import 'base.dart';
|
||||
|
||||
@@ -15,7 +15,7 @@ class MyMockClient extends Mock implements http.Client {
|
||||
if (requestType == 'POST' &&
|
||||
requestUri == OpenAIRequestType.textCompletion.uri) {
|
||||
final responseHeaders = <String, String>{
|
||||
'content-type': 'text/event-stream'
|
||||
'content-type': 'text/event-stream',
|
||||
};
|
||||
final responseBody = Stream.fromIterable([
|
||||
utf8.encode(
|
||||
|
||||
@@ -59,6 +59,8 @@ PODS:
|
||||
- Flutter
|
||||
- irondash_engine_context (0.0.1):
|
||||
- Flutter
|
||||
- keyboard_height_plugin (0.0.1):
|
||||
- Flutter
|
||||
- package_info_plus (0.4.5):
|
||||
- Flutter
|
||||
- path_provider_foundation (0.0.1):
|
||||
@@ -100,6 +102,7 @@ DEPENDENCIES:
|
||||
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
|
||||
- integration_test (from `.symlinks/plugins/integration_test/ios`)
|
||||
- irondash_engine_context (from `.symlinks/plugins/irondash_engine_context/ios`)
|
||||
- keyboard_height_plugin (from `.symlinks/plugins/keyboard_height_plugin/ios`)
|
||||
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
|
||||
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
|
||||
- rich_clipboard_ios (from `.symlinks/plugins/rich_clipboard_ios/ios`)
|
||||
@@ -145,6 +148,8 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/integration_test/ios"
|
||||
irondash_engine_context:
|
||||
:path: ".symlinks/plugins/irondash_engine_context/ios"
|
||||
keyboard_height_plugin:
|
||||
:path: ".symlinks/plugins/keyboard_height_plugin/ios"
|
||||
package_info_plus:
|
||||
:path: ".symlinks/plugins/package_info_plus/ios"
|
||||
path_provider_foundation:
|
||||
@@ -171,7 +176,7 @@ SPEC CHECKSUMS:
|
||||
device_info_plus: 7545d84d8d1b896cb16a4ff98c19f07ec4b298ea
|
||||
DKImagePickerController: b512c28220a2b8ac7419f21c491fc8534b7601ac
|
||||
DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179
|
||||
file_picker: ce3938a0df3cc1ef404671531facef740d03f920
|
||||
file_picker: 15fd9539e4eb735dc54bae8c0534a7a9511a03de
|
||||
flowy_infra_ui: 0455e1fa8c51885aa1437848e361e99419f34ebc
|
||||
Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854
|
||||
fluttertoast: fafc4fa4d01a6a9e4f772ecd190ffa525e9e2d9c
|
||||
@@ -180,6 +185,7 @@ SPEC CHECKSUMS:
|
||||
image_picker_ios: 4a8aadfbb6dc30ad5141a2ce3832af9214a705b5
|
||||
integration_test: 13825b8a9334a850581300559b8839134b124670
|
||||
irondash_engine_context: 3458bf979b90d616ffb8ae03a150bafe2e860cc9
|
||||
keyboard_height_plugin: 43fa8bba20fd5c4fdeed5076466b8b9d43cc6b86
|
||||
package_info_plus: fd030dabf36271f146f1f3beacd48f564b0f17f7
|
||||
path_provider_foundation: eaf5b3e458fc0e5fbb9940fb09980e853fe058b8
|
||||
ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825
|
||||
|
||||
@@ -143,7 +143,7 @@
|
||||
97C146E61CF9000F007C117D /* Project object */ = {
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
LastUpgradeCheck = 1300;
|
||||
LastUpgradeCheck = 1430;
|
||||
ORGANIZATIONNAME = "";
|
||||
TargetAttributes = {
|
||||
97C146ED1CF9000F007C117D = {
|
||||
@@ -515,7 +515,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEAD_CODE_STRIPPING = NO;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
DEVELOPMENT_TEAM = VHB67HRSZG;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = AppFlowy;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1300"
|
||||
LastUpgradeVersion = "1430"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
|
||||
|
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 6.4 KiB After Width: | Height: | Size: 6.2 KiB |
|
Before Width: | Height: | Size: 6.8 KiB After Width: | Height: | Size: 6.6 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 4.3 KiB After Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 4.7 KiB After Width: | Height: | Size: 4.6 KiB |
@@ -1 +1,80 @@
|
||||
{"images":[{"size":"60x60","expected-size":"180","filename":"180.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"40x40","expected-size":"80","filename":"80.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"40x40","expected-size":"120","filename":"120.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"60x60","expected-size":"120","filename":"120.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"57x57","expected-size":"57","filename":"57.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"1x"},{"size":"29x29","expected-size":"58","filename":"58.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"29x29","expected-size":"29","filename":"29.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"1x"},{"size":"29x29","expected-size":"87","filename":"87.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"57x57","expected-size":"114","filename":"114.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"20x20","expected-size":"40","filename":"40.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"20x20","expected-size":"60","filename":"60.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"1024x1024","filename":"1024.png","expected-size":"1024","idiom":"ios-marketing","folder":"Assets.xcassets/AppIcon.appiconset/","scale":"1x"}]}
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "40.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"filename" : "60.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "3x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"filename" : "29.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "1x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"filename" : "58.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"filename" : "87.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "3x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"filename" : "80.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"filename" : "120.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "3x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"filename" : "57.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "1x",
|
||||
"size" : "57x57"
|
||||
},
|
||||
{
|
||||
"filename" : "114.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x",
|
||||
"size" : "57x57"
|
||||
},
|
||||
{
|
||||
"filename" : "120.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x",
|
||||
"size" : "60x60"
|
||||
},
|
||||
{
|
||||
"filename" : "180.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "3x",
|
||||
"size" : "60x60"
|
||||
},
|
||||
{
|
||||
"filename" : "1024.png",
|
||||
"idiom" : "ios-marketing",
|
||||
"scale" : "1x",
|
||||
"size" : "1024x1024"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,10 @@
|
||||
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-config/entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:dartz/dartz.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
abstract class KeyValueStorage {
|
||||
Future<void> set(String key, String value);
|
||||
Future<Either<FlowyError, String>> get(String key);
|
||||
Future<Either<FlowyError, T>> getWithFormat<T>(
|
||||
Future<Option<String>> get(String key);
|
||||
Future<Option<T>> getWithFormat<T>(
|
||||
String key,
|
||||
T Function(String value) formatter,
|
||||
);
|
||||
@@ -20,25 +17,25 @@ class DartKeyValue implements KeyValueStorage {
|
||||
SharedPreferences get sharedPreferences => _sharedPreferences!;
|
||||
|
||||
@override
|
||||
Future<Either<FlowyError, String>> get(String key) async {
|
||||
Future<Option<String>> get(String key) async {
|
||||
await _initSharedPreferencesIfNeeded();
|
||||
|
||||
final value = sharedPreferences.getString(key);
|
||||
if (value != null) {
|
||||
return Right(value);
|
||||
return Some(value);
|
||||
}
|
||||
return Left(FlowyError());
|
||||
return none();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Either<FlowyError, T>> getWithFormat<T>(
|
||||
Future<Option<T>> getWithFormat<T>(
|
||||
String key,
|
||||
T Function(String value) formatter,
|
||||
) async {
|
||||
final value = await get(key);
|
||||
return value.fold(
|
||||
(l) => left(l),
|
||||
(r) => right(formatter(r)),
|
||||
() => none(),
|
||||
(s) => Some(formatter(s)),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -67,47 +64,3 @@ class DartKeyValue implements KeyValueStorage {
|
||||
_sharedPreferences ??= await SharedPreferences.getInstance();
|
||||
}
|
||||
}
|
||||
|
||||
/// Key-value store
|
||||
/// The data is stored in the local storage of the device.
|
||||
class RustKeyValue implements KeyValueStorage {
|
||||
@override
|
||||
Future<void> set(String key, String value) async {
|
||||
await ConfigEventSetKeyValue(
|
||||
KeyValuePB.create()
|
||||
..key = key
|
||||
..value = value,
|
||||
).send();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Either<FlowyError, String>> get(String key) async {
|
||||
final payload = KeyPB.create()..key = key;
|
||||
final response = await ConfigEventGetKeyValue(payload).send();
|
||||
return response.swap().map((r) => r.value);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Either<FlowyError, T>> getWithFormat<T>(
|
||||
String key,
|
||||
T Function(String value) formatter,
|
||||
) async {
|
||||
final value = await get(key);
|
||||
return value.fold(
|
||||
(l) => left(l),
|
||||
(r) => right(formatter(r)),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> remove(String key) async {
|
||||
await ConfigEventRemoveKeyValue(
|
||||
KeyPB.create()..key = key,
|
||||
).send();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> clear() async {
|
||||
// TODO(Lucas): implement clear
|
||||
}
|
||||
}
|
||||
@@ -6,13 +6,6 @@ class KVKeys {
|
||||
/// The key for the path location of the local data for the whole app.
|
||||
static const String pathLocation = '$prefix.path_location';
|
||||
|
||||
/// The key for the last time login type.
|
||||
///
|
||||
/// The value is one of the following:
|
||||
/// - local
|
||||
/// - supabase
|
||||
static const String loginType = '$prefix.login_type';
|
||||
|
||||
/// The key for saving the window size
|
||||
///
|
||||
/// The value is a json string with the following format:
|
||||
@@ -49,4 +42,9 @@ class KVKeys {
|
||||
/// The value is a boolean string.
|
||||
static const String showRenameDialogWhenCreatingNewFile =
|
||||
'showRenameDialogWhenCreatingNewFile';
|
||||
|
||||
static const String kCloudType = 'kCloudType';
|
||||
static const String kAppflowyCloudBaseURL = 'kAppFlowyCloudBaseURL';
|
||||
static const String kSupabaseURL = 'kSupbaseURL';
|
||||
static const String kSupabaseAnonKey = 'kSupabaseAnonKey';
|
||||
}
|
||||
@@ -4,30 +4,40 @@ import 'package:json_annotation/json_annotation.dart';
|
||||
part 'backend_env.g.dart';
|
||||
|
||||
@JsonSerializable()
|
||||
class AppFlowyEnv {
|
||||
class AppFlowyConfiguration {
|
||||
final String root;
|
||||
final String custom_app_path;
|
||||
final String origin_app_path;
|
||||
final String device_id;
|
||||
final int authenticator_type;
|
||||
final SupabaseConfiguration supabase_config;
|
||||
final AppFlowyCloudConfiguration appflowy_cloud_config;
|
||||
final Map<String, String> envs;
|
||||
|
||||
AppFlowyEnv({
|
||||
AppFlowyConfiguration({
|
||||
required this.root,
|
||||
required this.custom_app_path,
|
||||
required this.origin_app_path,
|
||||
required this.device_id,
|
||||
required this.authenticator_type,
|
||||
required this.supabase_config,
|
||||
required this.appflowy_cloud_config,
|
||||
required this.envs,
|
||||
});
|
||||
|
||||
factory AppFlowyEnv.fromJson(Map<String, dynamic> json) =>
|
||||
_$AppFlowyEnvFromJson(json);
|
||||
factory AppFlowyConfiguration.fromJson(Map<String, dynamic> json) =>
|
||||
_$AppFlowyConfigurationFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$AppFlowyEnvToJson(this);
|
||||
Map<String, dynamic> toJson() => _$AppFlowyConfigurationToJson(this);
|
||||
}
|
||||
|
||||
@JsonSerializable()
|
||||
class SupabaseConfiguration {
|
||||
/// Indicates whether the sync feature is enabled.
|
||||
final bool enable_sync;
|
||||
final String url;
|
||||
final String anon_key;
|
||||
|
||||
SupabaseConfiguration({
|
||||
this.enable_sync = true,
|
||||
required this.url,
|
||||
required this.anon_key,
|
||||
});
|
||||
@@ -36,6 +46,17 @@ class SupabaseConfiguration {
|
||||
_$SupabaseConfigurationFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$SupabaseConfigurationToJson(this);
|
||||
|
||||
static SupabaseConfiguration defaultConfig() {
|
||||
return SupabaseConfiguration(
|
||||
url: '',
|
||||
anon_key: '',
|
||||
);
|
||||
}
|
||||
|
||||
bool get isValid {
|
||||
return url.isNotEmpty && anon_key.isNotEmpty;
|
||||
}
|
||||
}
|
||||
|
||||
@JsonSerializable()
|
||||
@@ -54,4 +75,18 @@ class AppFlowyCloudConfiguration {
|
||||
_$AppFlowyCloudConfigurationFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$AppFlowyCloudConfigurationToJson(this);
|
||||
|
||||
static AppFlowyCloudConfiguration defaultConfig() {
|
||||
return AppFlowyCloudConfiguration(
|
||||
base_url: '',
|
||||
ws_base_url: '',
|
||||
gotrue_url: '',
|
||||
);
|
||||
}
|
||||
|
||||
bool get isValid {
|
||||
return base_url.isNotEmpty &&
|
||||
ws_base_url.isNotEmpty &&
|
||||
gotrue_url.isNotEmpty;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,320 @@
|
||||
import 'package:appflowy/core/config/kv.dart';
|
||||
import 'package:appflowy/core/config/kv_keys.dart';
|
||||
import 'package:appflowy/env/backend_env.dart';
|
||||
import 'package:appflowy/env/env.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:dartz/dartz.dart';
|
||||
|
||||
/// Sets the cloud type for the application.
|
||||
///
|
||||
/// This method updates the cloud type setting in the key-value storage
|
||||
/// using the [KeyValueStorage] service. The cloud type is identified
|
||||
/// by the [AuthenticatorType] enum.
|
||||
///
|
||||
/// [ty] - The type of cloud to be set. It must be one of the values from
|
||||
/// [AuthenticatorType] enum. The corresponding integer value of the enum is stored:
|
||||
/// - `CloudType.local` is stored as "0".
|
||||
/// - `CloudType.supabase` is stored as "1".
|
||||
/// - `CloudType.appflowyCloud` is stored as "2".
|
||||
Future<void> setAuthenticatorType(AuthenticatorType ty) async {
|
||||
switch (ty) {
|
||||
case AuthenticatorType.local:
|
||||
getIt<KeyValueStorage>().set(KVKeys.kCloudType, 0.toString());
|
||||
break;
|
||||
case AuthenticatorType.supabase:
|
||||
getIt<KeyValueStorage>().set(KVKeys.kCloudType, 1.toString());
|
||||
break;
|
||||
case AuthenticatorType.appflowyCloud:
|
||||
getIt<KeyValueStorage>().set(KVKeys.kCloudType, 2.toString());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// Retrieves the currently set cloud type.
|
||||
///
|
||||
/// This method fetches the cloud type setting from the key-value storage
|
||||
/// using the [KeyValueStorage] service and returns the corresponding
|
||||
/// [AuthenticatorType] enum value.
|
||||
///
|
||||
/// Returns:
|
||||
/// A Future that resolves to a [AuthenticatorType] enum value representing the
|
||||
/// currently set cloud type. The default return value is `CloudType.local`
|
||||
/// if no valid setting is found.
|
||||
///
|
||||
Future<AuthenticatorType> getAuthenticatorType() async {
|
||||
final value = await getIt<KeyValueStorage>().get(KVKeys.kCloudType);
|
||||
return value.fold(() => AuthenticatorType.local, (s) {
|
||||
switch (s) {
|
||||
case "0":
|
||||
return AuthenticatorType.local;
|
||||
case "1":
|
||||
return AuthenticatorType.supabase;
|
||||
case "2":
|
||||
return AuthenticatorType.appflowyCloud;
|
||||
default:
|
||||
return AuthenticatorType.local;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Determines whether authentication is enabled.
|
||||
///
|
||||
/// This getter evaluates if authentication should be enabled based on the
|
||||
/// current integration mode and cloud type settings.
|
||||
///
|
||||
/// Returns:
|
||||
/// A boolean value indicating whether authentication is enabled. It returns
|
||||
/// `true` if the application is in release or develop mode, and the cloud type
|
||||
/// is not set to `CloudType.local`. Additionally, it checks if either the
|
||||
/// AppFlowy Cloud or Supabase configuration is valid.
|
||||
/// Returns `false` otherwise.
|
||||
bool get isAuthEnabled {
|
||||
// Only enable supabase in release and develop mode.
|
||||
if (integrationMode().isRelease ||
|
||||
integrationMode().isDevelop ||
|
||||
integrationMode().isIntegrationTest) {
|
||||
final env = getIt<AppFlowyCloudSharedEnv>();
|
||||
if (env.authenticatorType == AuthenticatorType.supabase) {
|
||||
return env.supabaseConfig.isValid;
|
||||
}
|
||||
|
||||
if (env.authenticatorType == AuthenticatorType.appflowyCloud) {
|
||||
return env.appflowyCloudConfig.isValid;
|
||||
}
|
||||
|
||||
return false;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks if Supabase is enabled.
|
||||
///
|
||||
/// This getter evaluates if Supabase should be enabled based on the
|
||||
/// current integration mode and cloud type setting.
|
||||
///
|
||||
/// Returns:
|
||||
/// A boolean value indicating whether Supabase is enabled. It returns `true`
|
||||
/// if the application is in release or develop mode and the current cloud type
|
||||
/// is `CloudType.supabase`. Otherwise, it returns `false`.
|
||||
bool get isSupabaseEnabled {
|
||||
// Only enable supabase in release and develop mode.
|
||||
if (integrationMode().isRelease ||
|
||||
integrationMode().isDevelop ||
|
||||
integrationMode().isIntegrationTest) {
|
||||
return currentCloudType() == AuthenticatorType.supabase;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Determines if AppFlowy Cloud is enabled.
|
||||
///
|
||||
/// This getter assesses if AppFlowy Cloud should be enabled based on the
|
||||
/// current integration mode and cloud type setting.
|
||||
///
|
||||
/// Returns:
|
||||
/// A boolean value indicating whether AppFlowy Cloud is enabled. It returns
|
||||
/// `true` if the application is in release or develop mode and the current
|
||||
/// cloud type is `CloudType.appflowyCloud`. Otherwise, it returns `false`.
|
||||
bool get isAppFlowyCloudEnabled {
|
||||
// Only enable appflowy cloud in release and develop mode.
|
||||
if (integrationMode().isRelease ||
|
||||
integrationMode().isDevelop ||
|
||||
integrationMode().isIntegrationTest) {
|
||||
return currentCloudType() == AuthenticatorType.appflowyCloud;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
enum AuthenticatorType {
|
||||
local,
|
||||
supabase,
|
||||
appflowyCloud;
|
||||
|
||||
bool get isEnabled => this != AuthenticatorType.local;
|
||||
int get value {
|
||||
switch (this) {
|
||||
case AuthenticatorType.local:
|
||||
return 0;
|
||||
case AuthenticatorType.supabase:
|
||||
return 1;
|
||||
case AuthenticatorType.appflowyCloud:
|
||||
return 2;
|
||||
}
|
||||
}
|
||||
|
||||
static fromValue(int value) {
|
||||
switch (value) {
|
||||
case 0:
|
||||
return AuthenticatorType.local;
|
||||
case 1:
|
||||
return AuthenticatorType.supabase;
|
||||
case 2:
|
||||
return AuthenticatorType.appflowyCloud;
|
||||
default:
|
||||
return AuthenticatorType.local;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AuthenticatorType currentCloudType() {
|
||||
return getIt<AppFlowyCloudSharedEnv>().authenticatorType;
|
||||
}
|
||||
|
||||
Future<void> setAppFlowyCloudUrl(Option<String> url) async {
|
||||
await url.fold(
|
||||
() => getIt<KeyValueStorage>().remove(KVKeys.kAppflowyCloudBaseURL),
|
||||
(s) => getIt<KeyValueStorage>().set(KVKeys.kAppflowyCloudBaseURL, s),
|
||||
);
|
||||
}
|
||||
|
||||
/// Use getIt<AppFlowyCloudSharedEnv>() to get the shared environment.
|
||||
class AppFlowyCloudSharedEnv {
|
||||
final AuthenticatorType _authenticatorType;
|
||||
final AppFlowyCloudConfiguration appflowyCloudConfig;
|
||||
final SupabaseConfiguration supabaseConfig;
|
||||
|
||||
AppFlowyCloudSharedEnv({
|
||||
required AuthenticatorType authenticatorType,
|
||||
required this.appflowyCloudConfig,
|
||||
required this.supabaseConfig,
|
||||
}) : _authenticatorType = authenticatorType;
|
||||
|
||||
AuthenticatorType get authenticatorType => _authenticatorType;
|
||||
|
||||
static Future<AppFlowyCloudSharedEnv> fromEnv() async {
|
||||
// If [Env.enableCustomCloud] is true, then use the custom cloud configuration.
|
||||
if (Env.enableCustomCloud) {
|
||||
// Use the custom cloud configuration.
|
||||
final cloudType = await getAuthenticatorType();
|
||||
final appflowyCloudConfig = await getAppFlowyCloudConfig();
|
||||
final supabaseCloudConfig = await getSupabaseCloudConfig();
|
||||
|
||||
return AppFlowyCloudSharedEnv(
|
||||
authenticatorType: cloudType,
|
||||
appflowyCloudConfig: appflowyCloudConfig,
|
||||
supabaseConfig: supabaseCloudConfig,
|
||||
);
|
||||
} else {
|
||||
// Using the cloud settings from the .env file.
|
||||
final appflowyCloudConfig = AppFlowyCloudConfiguration(
|
||||
base_url: Env.afCloudUrl,
|
||||
ws_base_url: await _getAppFlowyCloudWSUrl(Env.afCloudUrl),
|
||||
gotrue_url: await _getAppFlowyCloudGotrueUrl(Env.afCloudUrl),
|
||||
);
|
||||
|
||||
return AppFlowyCloudSharedEnv(
|
||||
authenticatorType: AuthenticatorType.fromValue(Env.authenticatorType),
|
||||
appflowyCloudConfig: appflowyCloudConfig,
|
||||
supabaseConfig: SupabaseConfiguration.defaultConfig(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<AppFlowyCloudConfiguration> configurationFromUri(
|
||||
Uri baseUri,
|
||||
String baseUrl,
|
||||
) async {
|
||||
// When the host is set to 'localhost', the application will utilize the local configuration. This setup assumes that 'localhost' does not employ a reverse proxy, therefore default port settings are used.
|
||||
if (baseUri.host == "localhost") {
|
||||
return AppFlowyCloudConfiguration(
|
||||
base_url: "$baseUrl:8000",
|
||||
ws_base_url: "ws://${baseUri.host}:8000/ws",
|
||||
gotrue_url: "$baseUrl:9998",
|
||||
);
|
||||
} else {
|
||||
return AppFlowyCloudConfiguration(
|
||||
base_url: baseUrl,
|
||||
ws_base_url: await _getAppFlowyCloudWSUrl(baseUrl),
|
||||
gotrue_url: await _getAppFlowyCloudGotrueUrl(baseUrl),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<AppFlowyCloudConfiguration> getAppFlowyCloudConfig() async {
|
||||
final baseURL = await getAppFlowyCloudUrl();
|
||||
|
||||
try {
|
||||
final uri = Uri.parse(baseURL);
|
||||
return await configurationFromUri(uri, baseURL);
|
||||
} catch (e) {
|
||||
Log.error("Failed to parse AppFlowy Cloud URL: $e");
|
||||
return AppFlowyCloudConfiguration.defaultConfig();
|
||||
}
|
||||
}
|
||||
|
||||
Future<String> getAppFlowyCloudUrl() async {
|
||||
final result =
|
||||
await getIt<KeyValueStorage>().get(KVKeys.kAppflowyCloudBaseURL);
|
||||
return result.fold(
|
||||
() => "",
|
||||
(url) => url,
|
||||
);
|
||||
}
|
||||
|
||||
Future<String> _getAppFlowyCloudWSUrl(String baseURL) async {
|
||||
try {
|
||||
final uri = Uri.parse(baseURL);
|
||||
|
||||
// Construct the WebSocket URL directly from the parsed URI.
|
||||
final wsScheme = uri.isScheme('HTTPS') ? 'wss' : 'ws';
|
||||
final wsUrl = Uri(scheme: wsScheme, host: uri.host, path: '/ws');
|
||||
|
||||
return wsUrl.toString();
|
||||
} catch (e) {
|
||||
Log.error("Failed to get WebSocket URL: $e");
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
Future<String> _getAppFlowyCloudGotrueUrl(String baseURL) async {
|
||||
return "$baseURL/gotrue";
|
||||
}
|
||||
|
||||
Future<void> setSupbaseServer(
|
||||
Option<String> url,
|
||||
Option<String> anonKey,
|
||||
) async {
|
||||
assert(
|
||||
(url.isSome() && anonKey.isSome()) || (url.isNone() && anonKey.isNone()),
|
||||
"Either both Supabase URL and anon key must be set, or both should be unset",
|
||||
);
|
||||
|
||||
await url.fold(
|
||||
() => getIt<KeyValueStorage>().remove(KVKeys.kSupabaseURL),
|
||||
(s) => getIt<KeyValueStorage>().set(KVKeys.kSupabaseURL, s),
|
||||
);
|
||||
await anonKey.fold(
|
||||
() => getIt<KeyValueStorage>().remove(KVKeys.kSupabaseAnonKey),
|
||||
(s) => getIt<KeyValueStorage>().set(KVKeys.kSupabaseAnonKey, s),
|
||||
);
|
||||
}
|
||||
|
||||
Future<SupabaseConfiguration> getSupabaseCloudConfig() async {
|
||||
final url = await _getSupbaseUrl();
|
||||
final anonKey = await _getSupabaseAnonKey();
|
||||
return SupabaseConfiguration(
|
||||
url: url,
|
||||
anon_key: anonKey,
|
||||
);
|
||||
}
|
||||
|
||||
Future<String> _getSupbaseUrl() async {
|
||||
final result = await getIt<KeyValueStorage>().get(KVKeys.kSupabaseURL);
|
||||
return result.fold(
|
||||
() => "",
|
||||
(url) => url,
|
||||
);
|
||||
}
|
||||
|
||||
Future<String> _getSupabaseAnonKey() async {
|
||||
final result = await getIt<KeyValueStorage>().get(KVKeys.kSupabaseAnonKey);
|
||||
return result.fold(
|
||||
() => "",
|
||||
(url) => url,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
// lib/env/env.dart
|
||||
import 'package:envied/envied.dart';
|
||||
|
||||
part 'cloud_env_test.g.dart';
|
||||
|
||||
/// Follow the guide on https://supabase.com/docs/guides/auth/social-login/auth-google to setup the auth provider.
|
||||
///
|
||||
@Envied(path: '.env.cloud.test')
|
||||
abstract class TestEnv {
|
||||
/// AppFlowy Cloud Configuration
|
||||
@EnviedField(
|
||||
obfuscate: true,
|
||||
varName: 'APPFLOWY_CLOUD_URL',
|
||||
defaultValue: '',
|
||||
)
|
||||
static final String afCloudUrl = _TestEnv.afCloudUrl;
|
||||
|
||||
// Supabase Configuration:
|
||||
@EnviedField(
|
||||
obfuscate: true,
|
||||
varName: 'SUPABASE_URL',
|
||||
defaultValue: '',
|
||||
)
|
||||
static final String supabaseUrl = _TestEnv.supabaseUrl;
|
||||
@EnviedField(
|
||||
obfuscate: true,
|
||||
varName: 'SUPABASE_ANON_KEY',
|
||||
defaultValue: '',
|
||||
)
|
||||
static final String supabaseAnonKey = _TestEnv.supabaseAnonKey;
|
||||
}
|
||||
@@ -1,129 +1,29 @@
|
||||
// lib/env/env.dart
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy/env/cloud_env.dart';
|
||||
import 'package:envied/envied.dart';
|
||||
|
||||
part 'env.g.dart';
|
||||
|
||||
/// The environment variables are defined in `.env` file that is located in the
|
||||
/// appflowy_flutter.
|
||||
/// Run `dart run build_runner build --delete-conflicting-outputs`
|
||||
/// to generate the keys from the env file.
|
||||
///
|
||||
/// If you want to regenerate the keys, you need to run `dart run
|
||||
/// build_runner clean` before running `dart run build_runner build
|
||||
/// --delete-conflicting-outputs`.
|
||||
|
||||
/// Follow the guide on https://supabase.com/docs/guides/auth/social-login/auth-google to setup the auth provider.
|
||||
///
|
||||
@Envied(path: '.env')
|
||||
abstract class Env {
|
||||
// This flag is used to decide if users can dynamically configure cloud settings. It turns true when a .env file exists containing the APPFLOWY_CLOUD_URL variable. By default, this is set to false.
|
||||
static bool get enableCustomCloud {
|
||||
return Env.authenticatorType == AuthenticatorType.appflowyCloud.value &&
|
||||
_Env.afCloudUrl.isEmpty;
|
||||
}
|
||||
|
||||
@EnviedField(
|
||||
obfuscate: true,
|
||||
varName: 'CLOUD_TYPE',
|
||||
defaultValue: '0',
|
||||
obfuscate: false,
|
||||
varName: 'AUTHENTICATOR_TYPE',
|
||||
defaultValue: 2,
|
||||
)
|
||||
static final int cloudType = _Env.cloudType;
|
||||
static const int authenticatorType = _Env.authenticatorType;
|
||||
|
||||
/// AppFlowy Cloud Configuration
|
||||
@EnviedField(
|
||||
obfuscate: true,
|
||||
varName: 'APPFLOWY_CLOUD_BASE_URL',
|
||||
obfuscate: false,
|
||||
varName: 'APPFLOWY_CLOUD_URL',
|
||||
defaultValue: '',
|
||||
)
|
||||
static final String afCloudBaseUrl = _Env.afCloudBaseUrl;
|
||||
|
||||
@EnviedField(
|
||||
obfuscate: true,
|
||||
varName: 'APPFLOWY_CLOUD_WS_BASE_URL',
|
||||
defaultValue: '',
|
||||
)
|
||||
static final String afCloudWSBaseUrl = _Env.afCloudWSBaseUrl;
|
||||
|
||||
@EnviedField(
|
||||
obfuscate: true,
|
||||
varName: 'APPFLOWY_CLOUD_GOTRUE_URL',
|
||||
defaultValue: '',
|
||||
)
|
||||
static final String afCloudGoTrueUrl = _Env.afCloudGoTrueUrl;
|
||||
|
||||
// Supabase Configuration:
|
||||
@EnviedField(
|
||||
obfuscate: true,
|
||||
varName: 'SUPABASE_URL',
|
||||
defaultValue: '',
|
||||
)
|
||||
static final String supabaseUrl = _Env.supabaseUrl;
|
||||
@EnviedField(
|
||||
obfuscate: true,
|
||||
varName: 'SUPABASE_ANON_KEY',
|
||||
defaultValue: '',
|
||||
)
|
||||
static final String supabaseAnonKey = _Env.supabaseAnonKey;
|
||||
}
|
||||
|
||||
bool get isCloudEnabled {
|
||||
// Only enable supabase in release and develop mode.
|
||||
if (integrationMode().isRelease || integrationMode().isDevelop) {
|
||||
return currentCloudType().isEnabled;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool get isSupabaseEnabled {
|
||||
// Only enable supabase in release and develop mode.
|
||||
if (integrationMode().isRelease || integrationMode().isDevelop) {
|
||||
return currentCloudType() == CloudType.supabase;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool get isAppFlowyCloudEnabled {
|
||||
// Only enable appflowy cloud in release and develop mode.
|
||||
if (integrationMode().isRelease || integrationMode().isDevelop) {
|
||||
return currentCloudType() == CloudType.appflowyCloud;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
enum CloudType {
|
||||
unknown,
|
||||
supabase,
|
||||
appflowyCloud;
|
||||
|
||||
bool get isEnabled => this != CloudType.unknown;
|
||||
}
|
||||
|
||||
CloudType currentCloudType() {
|
||||
final value = Env.cloudType;
|
||||
if (value == 1) {
|
||||
if (Env.supabaseUrl.isEmpty || Env.supabaseAnonKey.isEmpty) {
|
||||
Log.error(
|
||||
"Supabase is not configured correctly. The values are: "
|
||||
"url: ${Env.supabaseUrl}, anonKey: ${Env.supabaseAnonKey}",
|
||||
);
|
||||
return CloudType.unknown;
|
||||
} else {
|
||||
return CloudType.supabase;
|
||||
}
|
||||
}
|
||||
|
||||
if (value == 2) {
|
||||
if (Env.afCloudBaseUrl.isEmpty ||
|
||||
Env.afCloudWSBaseUrl.isEmpty ||
|
||||
Env.afCloudGoTrueUrl.isEmpty) {
|
||||
Log.error(
|
||||
"AppFlowy cloud is not configured correctly. The values are: "
|
||||
"baseUrl: ${Env.afCloudBaseUrl}, wsBaseUrl: ${Env.afCloudWSBaseUrl}, gotrueUrl: ${Env.afCloudGoTrueUrl}",
|
||||
);
|
||||
return CloudType.unknown;
|
||||
} else {
|
||||
return CloudType.appflowyCloud;
|
||||
}
|
||||
}
|
||||
|
||||
return CloudType.unknown;
|
||||
static const String afCloudUrl = _Env.afCloudUrl;
|
||||
}
|
||||
@@ -1,29 +1,22 @@
|
||||
import 'package:appflowy/mobile/presentation/database/mobile_board_screen.dart';
|
||||
import 'package:appflowy/mobile/presentation/database/board/mobile_board_screen.dart';
|
||||
import 'package:appflowy/mobile/presentation/database/mobile_calendar_screen.dart';
|
||||
import 'package:appflowy/mobile/presentation/database/mobile_grid_screen.dart';
|
||||
import 'package:appflowy/mobile/presentation/presentation.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
||||
import 'package:appflowy/workspace/application/recent/recent_service.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
class MobileRouterRecord {
|
||||
PropertyValueNotifier<String> lastPushedRouter =
|
||||
PropertyValueNotifier<String>('');
|
||||
}
|
||||
|
||||
extension MobileRouter on BuildContext {
|
||||
Future<void> pushView(ViewPB view) async {
|
||||
await FolderEventSetLatestView(ViewIdPB(value: view.id)).send();
|
||||
getIt<MobileRouterRecord>().lastPushedRouter.value = view.routeName;
|
||||
push(
|
||||
Uri(
|
||||
path: view.routeName,
|
||||
queryParameters: view.queryParameters,
|
||||
).toString(),
|
||||
);
|
||||
).then((value) {
|
||||
RecentService().updateRecentViews([view.id], true);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,270 +0,0 @@
|
||||
// ThemeData in mobile
|
||||
import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
|
||||
import 'package:flowy_infra/colorscheme/colorscheme.dart';
|
||||
import 'package:flowy_infra/size.dart';
|
||||
import 'package:flowy_infra/theme_extension.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
const _primaryColor = Color(0xFF2DA2F6); //primary 100
|
||||
const _onBackgroundColor = Color(0xff2F3030); // text/title color
|
||||
const _onSurfaceColor = Color(0xff676666); // text/body color
|
||||
const _onSecondaryColor = Color(0xFFC5C7CB); // text/body2 color
|
||||
|
||||
// TODO(yijing): improve theme color before release
|
||||
ThemeData getMobileThemeData(
|
||||
Brightness brightness,
|
||||
FlowyColorScheme theme,
|
||||
String fontFamily,
|
||||
String monospaceFontFamily,
|
||||
) {
|
||||
final mobileColorTheme = (brightness == Brightness.light)
|
||||
? ColorScheme(
|
||||
brightness: brightness,
|
||||
primary: _primaryColor,
|
||||
onPrimary: Colors.white,
|
||||
// TODO(yijing): add color later
|
||||
secondary: Colors.white,
|
||||
onSecondary: _onSecondaryColor,
|
||||
error: const Color(0xffFB006D),
|
||||
onError: const Color(0xffFB006D),
|
||||
background: Colors.white,
|
||||
onBackground: _onBackgroundColor,
|
||||
outline: const Color(0xffBDC0C5), //caption
|
||||
outlineVariant: const Color(0xffCBD5E0).withOpacity(0.24),
|
||||
//Snack bar
|
||||
surface: Colors.white,
|
||||
onSurface: _onSurfaceColor, // text/body color
|
||||
surfaceVariant: const Color.fromARGB(255, 216, 216, 216),
|
||||
)
|
||||
: ColorScheme(
|
||||
brightness: brightness,
|
||||
primary: _primaryColor,
|
||||
onPrimary: Colors.white,
|
||||
// TODO(yijing): add color later
|
||||
secondary: Colors.black,
|
||||
onSecondary: Colors.white,
|
||||
error: const Color(0xffFB006D),
|
||||
onError: const Color(0xffFB006D),
|
||||
background: const Color(0xff1C1C1E), // BG/Secondary color
|
||||
onBackground: Colors.white,
|
||||
outline: const Color(0xff96989C), //caption
|
||||
outlineVariant: Colors.black,
|
||||
//Snack bar
|
||||
surface: const Color(0xff2F3030),
|
||||
onSurface: const Color(0xffC5C6C7), // text/body color
|
||||
);
|
||||
|
||||
return ThemeData(
|
||||
// color
|
||||
primaryColor: mobileColorTheme.primary, //primary 100
|
||||
primaryColorLight: const Color(0xFF57B5F8), //primary 80
|
||||
dividerColor: mobileColorTheme.outline, //caption
|
||||
hintColor: mobileColorTheme.outline,
|
||||
disabledColor: mobileColorTheme.outline,
|
||||
scaffoldBackgroundColor: mobileColorTheme.background,
|
||||
appBarTheme: AppBarTheme(
|
||||
foregroundColor: mobileColorTheme.onBackground,
|
||||
backgroundColor: mobileColorTheme.background,
|
||||
centerTitle: false,
|
||||
titleTextStyle: TextStyle(
|
||||
color: mobileColorTheme.onBackground,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
letterSpacing: 0.05,
|
||||
),
|
||||
shadowColor: mobileColorTheme.outlineVariant,
|
||||
),
|
||||
radioTheme: RadioThemeData(
|
||||
fillColor: MaterialStateProperty.resolveWith((states) {
|
||||
if (states.contains(MaterialState.selected)) {
|
||||
return mobileColorTheme.primary;
|
||||
}
|
||||
return mobileColorTheme.outline;
|
||||
}),
|
||||
),
|
||||
// button
|
||||
elevatedButtonTheme: ElevatedButtonThemeData(
|
||||
style: ButtonStyle(
|
||||
fixedSize: MaterialStateProperty.all(const Size.fromHeight(48)),
|
||||
elevation: MaterialStateProperty.all(0),
|
||||
textStyle: MaterialStateProperty.all(
|
||||
const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
shadowColor: MaterialStateProperty.all(null),
|
||||
backgroundColor: MaterialStateProperty.resolveWith<Color>(
|
||||
(Set<MaterialState> states) {
|
||||
if (states.contains(MaterialState.disabled)) {
|
||||
return const Color(0xFF57B5F8);
|
||||
}
|
||||
return mobileColorTheme.primary;
|
||||
},
|
||||
),
|
||||
foregroundColor: MaterialStateProperty.all(Colors.white),
|
||||
),
|
||||
),
|
||||
outlinedButtonTheme: OutlinedButtonThemeData(
|
||||
style: ButtonStyle(
|
||||
textStyle: MaterialStateProperty.all(
|
||||
const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
foregroundColor: MaterialStateProperty.all(
|
||||
mobileColorTheme.onBackground,
|
||||
),
|
||||
backgroundColor: MaterialStateProperty.all(mobileColorTheme.background),
|
||||
shape: MaterialStateProperty.all(
|
||||
RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
),
|
||||
side: MaterialStateProperty.all(
|
||||
BorderSide(
|
||||
color: mobileColorTheme.outline,
|
||||
width: 0.5,
|
||||
),
|
||||
),
|
||||
padding: MaterialStateProperty.all(
|
||||
const EdgeInsets.symmetric(horizontal: 8, vertical: 12),
|
||||
),
|
||||
// splash color
|
||||
overlayColor: MaterialStateProperty.all(
|
||||
Colors.grey[100],
|
||||
),
|
||||
),
|
||||
),
|
||||
textButtonTheme: TextButtonThemeData(
|
||||
style: ButtonStyle(
|
||||
textStyle: MaterialStateProperty.all(
|
||||
const TextStyle(
|
||||
fontFamily: 'Poppins',
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
// text
|
||||
fontFamily: 'Poppins',
|
||||
textTheme: TextTheme(
|
||||
displayLarge: const TextStyle(
|
||||
color: Color(0xFF57B5F8),
|
||||
fontSize: 32,
|
||||
fontWeight: FontWeight.w700,
|
||||
height: 1.20,
|
||||
letterSpacing: 0.16,
|
||||
),
|
||||
displayMedium: TextStyle(
|
||||
color: mobileColorTheme.onBackground,
|
||||
fontSize: 32,
|
||||
fontWeight: FontWeight.w600,
|
||||
height: 1.20,
|
||||
letterSpacing: 0.16,
|
||||
),
|
||||
// H1 Semi 26
|
||||
displaySmall: TextStyle(
|
||||
color: mobileColorTheme.onBackground,
|
||||
fontWeight: FontWeight.w600,
|
||||
height: 1.10,
|
||||
letterSpacing: 0.13,
|
||||
),
|
||||
// body2 14 Regular
|
||||
bodyMedium: TextStyle(
|
||||
color: mobileColorTheme.onBackground,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w400,
|
||||
height: 1.20,
|
||||
letterSpacing: 0.07,
|
||||
),
|
||||
// Trash empty title
|
||||
labelLarge: TextStyle(
|
||||
color: mobileColorTheme.onBackground,
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.w600,
|
||||
letterSpacing: -0.3,
|
||||
),
|
||||
// setting item title
|
||||
labelMedium: TextStyle(
|
||||
color: mobileColorTheme.onSurface,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
// setting group title
|
||||
labelSmall: TextStyle(
|
||||
color: mobileColorTheme.onBackground,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
letterSpacing: 0.05,
|
||||
),
|
||||
),
|
||||
inputDecorationTheme: InputDecorationTheme(
|
||||
contentPadding: const EdgeInsets.all(8),
|
||||
focusedBorder: const OutlineInputBorder(
|
||||
borderSide: BorderSide(
|
||||
width: 2,
|
||||
color: _primaryColor,
|
||||
),
|
||||
borderRadius: BorderRadius.all(Radius.circular(6)),
|
||||
),
|
||||
focusedErrorBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(color: mobileColorTheme.error),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(6)),
|
||||
),
|
||||
errorBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(color: mobileColorTheme.error),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(6)),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(
|
||||
color: mobileColorTheme.outline,
|
||||
),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(6)),
|
||||
),
|
||||
),
|
||||
colorScheme: mobileColorTheme,
|
||||
indicatorColor: Colors.blue,
|
||||
extensions: [
|
||||
AFThemeExtension(
|
||||
warning: theme.yellow,
|
||||
success: theme.green,
|
||||
tint1: theme.tint1,
|
||||
tint2: theme.tint2,
|
||||
tint3: theme.tint3,
|
||||
tint4: theme.tint4,
|
||||
tint5: theme.tint5,
|
||||
tint6: theme.tint6,
|
||||
tint7: theme.tint7,
|
||||
tint8: theme.tint8,
|
||||
tint9: theme.tint9,
|
||||
textColor: theme.text,
|
||||
greyHover: theme.hoverBG1,
|
||||
greySelect: theme.bg3,
|
||||
lightGreyHover: theme.hoverBG3,
|
||||
toggleOffFill: theme.shader5,
|
||||
progressBarBGColor: theme.progressBarBGColor,
|
||||
toggleButtonBGColor: theme.toggleButtonBGColor,
|
||||
calendarWeekendBGColor: theme.calendarWeekendBGColor,
|
||||
gridRowCountColor: theme.gridRowCountColor,
|
||||
code: getFontStyle(
|
||||
fontFamily: monospaceFontFamily,
|
||||
fontColor: theme.shader3,
|
||||
),
|
||||
callout: getFontStyle(
|
||||
fontFamily: fontFamily,
|
||||
fontSize: FontSizes.s11,
|
||||
fontColor: theme.shader3,
|
||||
),
|
||||
calloutBGColor: theme.hoverBG3,
|
||||
tableCellBGColor: theme.surface,
|
||||
caption: getFontStyle(
|
||||
fontFamily: fontFamily,
|
||||
fontSize: FontSizes.s11,
|
||||
fontWeight: FontWeight.w400,
|
||||
fontColor: theme.hint,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,54 @@
|
||||
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:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
class AppBarBackButton extends StatelessWidget {
|
||||
const AppBarBackButton({
|
||||
super.key,
|
||||
this.onTap,
|
||||
});
|
||||
|
||||
final VoidCallback? onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AppBarButton(
|
||||
onTap: onTap ?? () => context.pop(),
|
||||
child: const Icon(Icons.arrow_back_ios_new),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AppBarCloseButton extends StatelessWidget {
|
||||
const AppBarCloseButton({
|
||||
super.key,
|
||||
this.onTap,
|
||||
this.margin = const EdgeInsets.symmetric(
|
||||
horizontal: 16.0,
|
||||
vertical: 12.0,
|
||||
),
|
||||
});
|
||||
|
||||
final VoidCallback? onTap;
|
||||
final EdgeInsets margin;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FlowyButton(
|
||||
useIntrinsicWidth: true,
|
||||
text: const Icon(
|
||||
Icons.close,
|
||||
),
|
||||
margin: margin,
|
||||
onTap: onTap ?? () => context.pop(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AppBarCancelButton extends StatelessWidget {
|
||||
const AppBarCancelButton({
|
||||
super.key,
|
||||
required this.onTap,
|
||||
});
|
||||
@@ -12,7 +59,9 @@ class AppBarBackButton extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return AppBarButton(
|
||||
onTap: onTap,
|
||||
child: const Icon(Icons.arrow_back_ios_new),
|
||||
child: FlowyText(
|
||||
LocaleKeys.button_cancel.tr(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -56,6 +105,9 @@ class AppBarButton extends StatelessWidget {
|
||||
return InkWell(
|
||||
enableFeedback: true,
|
||||
borderRadius: BorderRadius.circular(28),
|
||||
splashColor: Colors.transparent,
|
||||
focusColor: Colors.transparent,
|
||||
highlightColor: Colors.transparent,
|
||||
onTap: onTap,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(extent),
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/mobile/presentation/base/app_bar_actions.dart';
|
||||
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
|
||||
import 'package:appflowy/mobile/presentation/widgets/flowy_mobile_state_container.dart';
|
||||
import 'package:appflowy/plugins/base/emoji/emoji_text.dart';
|
||||
import 'package:appflowy/plugins/document/document_page.dart';
|
||||
import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_bloc.dart';
|
||||
@@ -92,20 +93,12 @@ class _MobileViewPageState extends State<MobileViewPage> {
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
final view = context.watch<ViewBloc>().state.view;
|
||||
return _buildApp(
|
||||
view,
|
||||
actions,
|
||||
body,
|
||||
);
|
||||
return _buildApp(view, actions, body);
|
||||
},
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return _buildApp(
|
||||
null,
|
||||
[],
|
||||
body,
|
||||
);
|
||||
return _buildApp(null, [], body);
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -120,8 +113,8 @@ class _MobileViewPageState extends State<MobileViewPage> {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (icon != null)
|
||||
FlowyText(
|
||||
'$icon ',
|
||||
EmojiText(
|
||||
emoji: '$icon ',
|
||||
fontSize: 22.0,
|
||||
),
|
||||
Expanded(
|
||||
@@ -133,9 +126,7 @@ class _MobileViewPageState extends State<MobileViewPage> {
|
||||
),
|
||||
],
|
||||
),
|
||||
leading: AppBarBackButton(
|
||||
onTap: () => context.pop(),
|
||||
),
|
||||
leading: const AppBarBackButton(),
|
||||
actions: actions,
|
||||
),
|
||||
body: SafeArea(
|
||||
@@ -148,7 +139,7 @@ class _MobileViewPageState extends State<MobileViewPage> {
|
||||
return AppBarMoreButton(
|
||||
onTap: (context) {
|
||||
showMobileBottomSheet(
|
||||
context: context,
|
||||
context,
|
||||
builder: (_) => _buildViewPageBottomSheet(context),
|
||||
);
|
||||
},
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/row/cells/select_option_cell/extension.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart';
|
||||
import 'package:flowy_infra/size.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class OptionColorList extends StatelessWidget {
|
||||
const OptionColorList({
|
||||
super.key,
|
||||
this.selectedColor,
|
||||
required this.onSelectedColor,
|
||||
});
|
||||
|
||||
final SelectOptionColorPB? selectedColor;
|
||||
final void Function(SelectOptionColorPB color) onSelectedColor;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GridView.count(
|
||||
crossAxisCount: 6,
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
children: SelectOptionColorPB.values.map(
|
||||
(colorPB) {
|
||||
final color = colorPB.toColor(context);
|
||||
final isSelected = selectedColor?.value == colorPB.value;
|
||||
return GestureDetector(
|
||||
onTap: () => onSelectedColor(colorPB),
|
||||
child: Container(
|
||||
margin: const EdgeInsets.all(
|
||||
8.0,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: color,
|
||||
borderRadius: Corners.s12Border,
|
||||
border: Border.all(
|
||||
color: isSelected
|
||||
? const Color(0xff00C6F1)
|
||||
: Theme.of(context).dividerColor,
|
||||
),
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: isSelected
|
||||
? const FlowySvg(
|
||||
FlowySvgs.blue_check_s,
|
||||
size: Size.square(28.0),
|
||||
blendMode: null,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
);
|
||||
},
|
||||
).toList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ export 'bottom_sheet_action_widget.dart';
|
||||
export 'bottom_sheet_add_new_page.dart';
|
||||
export 'bottom_sheet_drag_handler.dart';
|
||||
export 'bottom_sheet_rename_widget.dart';
|
||||
export 'bottom_sheet_view_item.dart';
|
||||
export 'bottom_sheet_view_item_body.dart';
|
||||
export 'bottom_sheet_view_item_header.dart';
|
||||
export 'bottom_sheet_view_page.dart';
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class BottomSheetActionWidget extends StatelessWidget {
|
||||
const BottomSheetActionWidget({
|
||||
super.key,
|
||||
required this.svg,
|
||||
this.svg,
|
||||
required this.text,
|
||||
required this.onTap,
|
||||
this.iconColor,
|
||||
});
|
||||
|
||||
final FlowySvgData svg;
|
||||
final FlowySvgData? svg;
|
||||
final String text;
|
||||
final VoidCallback onTap;
|
||||
final Color? iconColor;
|
||||
@@ -20,13 +21,30 @@ class BottomSheetActionWidget extends StatelessWidget {
|
||||
final iconColor =
|
||||
this.iconColor ?? Theme.of(context).colorScheme.onBackground;
|
||||
|
||||
if (svg == null) {
|
||||
return OutlinedButton(
|
||||
style: Theme.of(context)
|
||||
.outlinedButtonTheme
|
||||
.style
|
||||
?.copyWith(alignment: Alignment.center),
|
||||
onPressed: onTap,
|
||||
child: FlowyText(
|
||||
text,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return OutlinedButton.icon(
|
||||
icon: FlowySvg(
|
||||
svg,
|
||||
svg!,
|
||||
size: const Size.square(22.0),
|
||||
color: iconColor,
|
||||
),
|
||||
label: Text(text),
|
||||
label: FlowyText(
|
||||
text,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
style: Theme.of(context)
|
||||
.outlinedButtonTheme
|
||||
.style
|
||||
|
||||
@@ -25,7 +25,7 @@ class AddNewPageWidgetBottomSheet extends StatelessWidget {
|
||||
children: [
|
||||
Expanded(
|
||||
child: BottomSheetActionWidget(
|
||||
svg: FlowySvgs.documents_s,
|
||||
svg: FlowySvgs.document_s,
|
||||
text: LocaleKeys.document_menuName.tr(),
|
||||
onTap: () => onAction(ViewLayoutPB.Document),
|
||||
),
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.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';
|
||||
import 'package:string_validator/string_validator.dart';
|
||||
|
||||
class MobileBottomSheetEditLinkWidget extends StatefulWidget {
|
||||
const MobileBottomSheetEditLinkWidget({
|
||||
super.key,
|
||||
required this.text,
|
||||
required this.href,
|
||||
required this.onEdit,
|
||||
});
|
||||
|
||||
final String text;
|
||||
final String? href;
|
||||
final void Function(String text, String href) onEdit;
|
||||
|
||||
@override
|
||||
State<MobileBottomSheetEditLinkWidget> createState() =>
|
||||
_MobileBottomSheetEditLinkWidgetState();
|
||||
}
|
||||
|
||||
class _MobileBottomSheetEditLinkWidgetState
|
||||
extends State<MobileBottomSheetEditLinkWidget> {
|
||||
late final TextEditingController textController;
|
||||
late final TextEditingController hrefController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
textController = TextEditingController(
|
||||
text: widget.text,
|
||||
);
|
||||
hrefController = TextEditingController(
|
||||
text: widget.href,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
textController.dispose();
|
||||
hrefController.dispose();
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_buildTextField(textController, null),
|
||||
const VSpace(12.0),
|
||||
_buildTextField(hrefController, LocaleKeys.editor_linkTextHint.tr()),
|
||||
const VSpace(12.0),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: BottomSheetActionWidget(
|
||||
text: LocaleKeys.button_cancel.tr(),
|
||||
onTap: () => context.pop(),
|
||||
),
|
||||
),
|
||||
const HSpace(8),
|
||||
Expanded(
|
||||
child: BottomSheetActionWidget(
|
||||
text: LocaleKeys.button_done.tr(),
|
||||
onTap: () {
|
||||
widget.onEdit(textController.text, hrefController.text);
|
||||
},
|
||||
),
|
||||
),
|
||||
if (widget.href != null && isURL(widget.href)) ...[
|
||||
const HSpace(8),
|
||||
Expanded(
|
||||
child: BottomSheetActionWidget(
|
||||
text: LocaleKeys.editor_openLink.tr(),
|
||||
onTap: () {
|
||||
safeLaunchUrl(widget.href!);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTextField(
|
||||
TextEditingController controller,
|
||||
String? hintText,
|
||||
) {
|
||||
return SizedBox(
|
||||
height: 44.0,
|
||||
child: FlowyTextField(
|
||||
controller: controller,
|
||||
hintText: hintText,
|
||||
suffixIcon: Padding(
|
||||
padding: const EdgeInsets.all(4.0),
|
||||
child: FlowyButton(
|
||||
text: const FlowySvg(
|
||||
FlowySvgs.close_lg,
|
||||
),
|
||||
margin: EdgeInsets.zero,
|
||||
useIntrinsicWidth: true,
|
||||
onTap: () {
|
||||
controller.clear();
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -8,10 +8,12 @@ class MobileBottomSheetRenameWidget extends StatefulWidget {
|
||||
super.key,
|
||||
required this.name,
|
||||
required this.onRename,
|
||||
this.padding = const EdgeInsets.symmetric(horizontal: 12.0, vertical: 16.0),
|
||||
});
|
||||
|
||||
final String name;
|
||||
final void Function(String name) onRename;
|
||||
final EdgeInsets padding;
|
||||
|
||||
@override
|
||||
State<MobileBottomSheetRenameWidget> createState() =>
|
||||
@@ -39,36 +41,33 @@ class _MobileBottomSheetRenameWidgetState
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 4.0,
|
||||
vertical: 16.0,
|
||||
),
|
||||
padding: widget.padding,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const HSpace(8.0),
|
||||
Expanded(
|
||||
child: SizedBox(
|
||||
height: 44.0,
|
||||
height: 42.0,
|
||||
child: FlowyTextField(
|
||||
controller: controller,
|
||||
textInputAction: TextInputAction.done,
|
||||
onSubmitted: (text) => widget.onRename(text),
|
||||
),
|
||||
),
|
||||
),
|
||||
const HSpace(12.0),
|
||||
FlowyTextButton(
|
||||
LocaleKeys.button_edit.tr(),
|
||||
constraints: const BoxConstraints.tightFor(height: 42),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 12.0,
|
||||
horizontal: 16.0,
|
||||
),
|
||||
fontColor: Colors.white,
|
||||
fillColor: Colors.lightBlue.shade300,
|
||||
fillColor: Theme.of(context).primaryColor,
|
||||
onPressed: () {
|
||||
widget.onRename(controller.text);
|
||||
},
|
||||
),
|
||||
const HSpace(8.0),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
|
||||
import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_bloc.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder2/protobuf.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart' hide WidgetBuilder;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
enum MobileBottomSheetType {
|
||||
view,
|
||||
rename,
|
||||
}
|
||||
|
||||
class MobileViewItemBottomSheet extends StatefulWidget {
|
||||
const MobileViewItemBottomSheet({
|
||||
super.key,
|
||||
required this.view,
|
||||
this.defaultType = MobileBottomSheetType.view,
|
||||
});
|
||||
|
||||
final ViewPB view;
|
||||
final MobileBottomSheetType defaultType;
|
||||
|
||||
@override
|
||||
State<MobileViewItemBottomSheet> createState() =>
|
||||
_MobileViewItemBottomSheetState();
|
||||
}
|
||||
|
||||
class _MobileViewItemBottomSheetState extends State<MobileViewItemBottomSheet> {
|
||||
MobileBottomSheetType type = MobileBottomSheetType.view;
|
||||
|
||||
@override
|
||||
initState() {
|
||||
super.initState();
|
||||
|
||||
type = widget.defaultType;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// header
|
||||
_buildHeader(),
|
||||
const VSpace(16),
|
||||
// body
|
||||
_buildBody(),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeader() {
|
||||
switch (type) {
|
||||
case MobileBottomSheetType.view:
|
||||
case MobileBottomSheetType.rename:
|
||||
// header
|
||||
return MobileViewItemBottomSheetHeader(
|
||||
showBackButton: type != MobileBottomSheetType.view,
|
||||
view: widget.view,
|
||||
onBack: () {
|
||||
setState(() {
|
||||
type = MobileBottomSheetType.view;
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildBody() {
|
||||
switch (type) {
|
||||
case MobileBottomSheetType.view:
|
||||
return MobileViewItemBottomSheetBody(
|
||||
isFavorite: widget.view.isFavorite,
|
||||
onAction: (action) {
|
||||
switch (action) {
|
||||
case MobileViewItemBottomSheetBodyAction.rename:
|
||||
setState(() {
|
||||
type = MobileBottomSheetType.rename;
|
||||
});
|
||||
break;
|
||||
case MobileViewItemBottomSheetBodyAction.duplicate:
|
||||
context.pop();
|
||||
context.read<ViewBloc>().add(const ViewEvent.duplicate());
|
||||
break;
|
||||
case MobileViewItemBottomSheetBodyAction.share:
|
||||
// unimplemented
|
||||
context.pop();
|
||||
break;
|
||||
case MobileViewItemBottomSheetBodyAction.delete:
|
||||
context.pop();
|
||||
context.read<ViewBloc>().add(const ViewEvent.delete());
|
||||
|
||||
break;
|
||||
case MobileViewItemBottomSheetBodyAction.addToFavorites:
|
||||
case MobileViewItemBottomSheetBodyAction.removeFromFavorites:
|
||||
context.pop();
|
||||
context
|
||||
.read<FavoriteBloc>()
|
||||
.add(FavoriteEvent.toggle(widget.view));
|
||||
break;
|
||||
}
|
||||
},
|
||||
);
|
||||
case MobileBottomSheetType.rename:
|
||||
return MobileBottomSheetRenameWidget(
|
||||
name: widget.view.name,
|
||||
onRename: (name) {
|
||||
if (name != widget.view.name) {
|
||||
context.read<ViewBloc>().add(ViewEvent.rename(name));
|
||||
}
|
||||
context.pop();
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -98,7 +98,7 @@ class MobileViewItemBottomSheetBody extends StatelessWidget {
|
||||
? MobileViewItemBottomSheetBodyAction.removeFromFavorites
|
||||
: MobileViewItemBottomSheetBodyAction.addToFavorites,
|
||||
),
|
||||
)
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder2/protobuf.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
@@ -16,8 +17,8 @@ class MobileViewItemBottomSheetHeader extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
// back button,
|
||||
showBackButton
|
||||
@@ -31,23 +32,27 @@ class MobileViewItemBottomSheetHeader extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
: FlowyButton(
|
||||
useIntrinsicWidth: true,
|
||||
text: const Icon(
|
||||
Icons.close,
|
||||
),
|
||||
margin: EdgeInsets.zero,
|
||||
onTap: () {
|
||||
context.pop();
|
||||
},
|
||||
),
|
||||
// title
|
||||
Expanded(
|
||||
child: Text(
|
||||
ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: MediaQuery.of(context).size.width * 0.6,
|
||||
),
|
||||
child: FlowyText.medium(
|
||||
view.name,
|
||||
style: theme.textTheme.labelSmall,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
Icons.close,
|
||||
color: theme.hintColor,
|
||||
),
|
||||
onPressed: () {
|
||||
context.pop();
|
||||
},
|
||||
)
|
||||
const HSpace(24.0),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -43,18 +43,15 @@ class _ViewPageBottomSheetState extends State<ViewPageBottomSheet> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 16, 16, 32),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// header
|
||||
_buildHeader(),
|
||||
const VSpace(16),
|
||||
// body
|
||||
_buildBody(),
|
||||
],
|
||||
),
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// header
|
||||
_buildHeader(),
|
||||
const VSpace(16),
|
||||
// body
|
||||
_buildBody(),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -120,33 +117,6 @@ class MobileViewBottomSheetBody extends StatelessWidget {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
// undo, redo
|
||||
// Row(
|
||||
// mainAxisSize: MainAxisSize.max,
|
||||
// children: [
|
||||
// Expanded(
|
||||
// child: BottomSheetActionWidget(
|
||||
// svg: FlowySvgs.m_undo_m,
|
||||
// text: LocaleKeys.toolbar_undo.tr(),
|
||||
// onTap: () => onAction(
|
||||
// MobileViewBottomSheetBodyAction.undo,
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// const HSpace(8),
|
||||
// Expanded(
|
||||
// child: BottomSheetActionWidget(
|
||||
// svg: FlowySvgs.m_redo_m,
|
||||
// text: LocaleKeys.toolbar_redo.tr(),
|
||||
// onTap: () => onAction(
|
||||
// MobileViewBottomSheetBodyAction.redo,
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// const VSpace(8),
|
||||
|
||||
// rename, duplicate
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
@@ -217,16 +187,16 @@ class MobileViewBottomSheetBody extends StatelessWidget {
|
||||
: MobileViewBottomSheetBodyAction.addToFavorites,
|
||||
),
|
||||
),
|
||||
const VSpace(8),
|
||||
|
||||
// help center
|
||||
BottomSheetActionWidget(
|
||||
svg: FlowySvgs.m_help_center_m,
|
||||
text: LocaleKeys.button_helpCenter.tr(),
|
||||
onTap: () => onAction(
|
||||
MobileViewBottomSheetBodyAction.helpCenter,
|
||||
),
|
||||
),
|
||||
// Help Center
|
||||
// const VSpace(8),
|
||||
// BottomSheetActionWidget(
|
||||
// svg: FlowySvgs.m_help_center_m,
|
||||
// text: LocaleKeys.button_helpCenter.tr(),
|
||||
// onTap: () => onAction(
|
||||
// MobileViewBottomSheetBodyAction.helpCenter,
|
||||
// ),
|
||||
// ),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart';
|
||||
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
|
||||
import 'package:appflowy/mobile/presentation/page_item/mobile_slide_action_button.dart';
|
||||
import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_bloc.dart';
|
||||
@@ -51,7 +51,7 @@ enum MobilePaneActionType {
|
||||
final viewBloc = context.read<ViewBloc>();
|
||||
final favoriteBloc = context.read<FavoriteBloc>();
|
||||
showMobileBottomSheet(
|
||||
context: context,
|
||||
context,
|
||||
builder: (context) {
|
||||
return MultiBlocProvider(
|
||||
providers: [
|
||||
|
||||
@@ -1,134 +1,107 @@
|
||||
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
|
||||
import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_bloc.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder2/protobuf.dart';
|
||||
import 'package:appflowy/mobile/presentation/base/app_bar_actions.dart';
|
||||
import 'package:appflowy/plugins/base/drag_handler.dart';
|
||||
import 'package:flowy_infra/size.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart' hide WidgetBuilder;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
Future<void> showMobileBottomSheet({
|
||||
required BuildContext context,
|
||||
Future<T?> showMobileBottomSheet<T>(
|
||||
BuildContext context, {
|
||||
required WidgetBuilder builder,
|
||||
ShapeBorder? shape,
|
||||
bool isDragEnabled = true,
|
||||
bool resizeToAvoidBottomInset = true,
|
||||
EdgeInsets padding = const EdgeInsets.fromLTRB(16, 16, 16, 32),
|
||||
bool showDragHandle = false,
|
||||
bool showHeader = false,
|
||||
bool showCloseButton = false,
|
||||
String title = '', // only works if showHeader is true
|
||||
Color? backgroundColor,
|
||||
bool isScrollControlled = true,
|
||||
BoxConstraints? constraints,
|
||||
}) async {
|
||||
showModalBottomSheet(
|
||||
assert(() {
|
||||
if (showCloseButton || title.isNotEmpty) assert(showHeader);
|
||||
return true;
|
||||
}());
|
||||
|
||||
return showModalBottomSheet<T>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
enableDrag: true,
|
||||
isScrollControlled: isScrollControlled,
|
||||
enableDrag: isDragEnabled,
|
||||
useSafeArea: true,
|
||||
builder: builder,
|
||||
clipBehavior: Clip.antiAlias,
|
||||
backgroundColor: backgroundColor,
|
||||
constraints: constraints,
|
||||
shape: shape ??
|
||||
const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(
|
||||
top: Corners.s12Radius,
|
||||
),
|
||||
),
|
||||
builder: (context) {
|
||||
final List<Widget> children = [];
|
||||
|
||||
if (showDragHandle) {
|
||||
children.addAll([
|
||||
const VSpace(4),
|
||||
const DragHandler(),
|
||||
]);
|
||||
}
|
||||
|
||||
if (showHeader) {
|
||||
children.addAll([
|
||||
VSpace(padding.top),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
showCloseButton
|
||||
? Padding(
|
||||
padding: EdgeInsets.only(left: padding.left),
|
||||
child: const AppBarCloseButton(
|
||||
margin: EdgeInsets.zero,
|
||||
),
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
FlowyText(
|
||||
title,
|
||||
),
|
||||
showCloseButton
|
||||
? HSpace(padding.right + 24)
|
||||
: const SizedBox.shrink(),
|
||||
],
|
||||
),
|
||||
const VSpace(4),
|
||||
const Divider(),
|
||||
]);
|
||||
}
|
||||
|
||||
final child = builder(context);
|
||||
|
||||
if (resizeToAvoidBottomInset) {
|
||||
children.add(
|
||||
AnimatedPadding(
|
||||
padding: EdgeInsets.only(
|
||||
top: showHeader ? 0 : padding.top,
|
||||
left: padding.left,
|
||||
right: padding.right,
|
||||
bottom: padding.bottom + MediaQuery.of(context).viewInsets.bottom,
|
||||
),
|
||||
duration: Duration.zero,
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
children.add(child);
|
||||
}
|
||||
|
||||
if (children.length == 1) {
|
||||
return children.first;
|
||||
}
|
||||
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: children,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
enum MobileBottomSheetType {
|
||||
view,
|
||||
rename,
|
||||
}
|
||||
|
||||
class MobileViewItemBottomSheet extends StatefulWidget {
|
||||
const MobileViewItemBottomSheet({
|
||||
super.key,
|
||||
required this.view,
|
||||
this.defaultType = MobileBottomSheetType.view,
|
||||
});
|
||||
|
||||
final ViewPB view;
|
||||
final MobileBottomSheetType defaultType;
|
||||
|
||||
@override
|
||||
State<MobileViewItemBottomSheet> createState() =>
|
||||
_MobileViewItemBottomSheetState();
|
||||
}
|
||||
|
||||
class _MobileViewItemBottomSheetState extends State<MobileViewItemBottomSheet> {
|
||||
MobileBottomSheetType type = MobileBottomSheetType.view;
|
||||
|
||||
@override
|
||||
initState() {
|
||||
super.initState();
|
||||
|
||||
type = widget.defaultType;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 16, 16, 32),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// header
|
||||
_buildHeader(),
|
||||
const VSpace(16),
|
||||
// body
|
||||
_buildBody(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeader() {
|
||||
switch (type) {
|
||||
case MobileBottomSheetType.view:
|
||||
case MobileBottomSheetType.rename:
|
||||
// header
|
||||
return MobileViewItemBottomSheetHeader(
|
||||
showBackButton: type != MobileBottomSheetType.view,
|
||||
view: widget.view,
|
||||
onBack: () {
|
||||
setState(() {
|
||||
type = MobileBottomSheetType.view;
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildBody() {
|
||||
switch (type) {
|
||||
case MobileBottomSheetType.view:
|
||||
return MobileViewItemBottomSheetBody(
|
||||
isFavorite: widget.view.isFavorite,
|
||||
onAction: (action) {
|
||||
switch (action) {
|
||||
case MobileViewItemBottomSheetBodyAction.rename:
|
||||
setState(() {
|
||||
type = MobileBottomSheetType.rename;
|
||||
});
|
||||
break;
|
||||
case MobileViewItemBottomSheetBodyAction.duplicate:
|
||||
context.pop();
|
||||
context.read<ViewBloc>().add(const ViewEvent.duplicate());
|
||||
break;
|
||||
case MobileViewItemBottomSheetBodyAction.share:
|
||||
// unimplemented
|
||||
context.pop();
|
||||
break;
|
||||
case MobileViewItemBottomSheetBodyAction.delete:
|
||||
context.pop();
|
||||
context.read<ViewBloc>().add(const ViewEvent.delete());
|
||||
|
||||
break;
|
||||
case MobileViewItemBottomSheetBodyAction.addToFavorites:
|
||||
case MobileViewItemBottomSheetBodyAction.removeFromFavorites:
|
||||
context.pop();
|
||||
context
|
||||
.read<FavoriteBloc>()
|
||||
.add(FavoriteEvent.toggle(widget.view));
|
||||
break;
|
||||
}
|
||||
},
|
||||
);
|
||||
case MobileBottomSheetType.rename:
|
||||
return MobileBottomSheetRenameWidget(
|
||||
name: widget.view.name,
|
||||
onRename: (name) {
|
||||
if (name != widget.view.name) {
|
||||
context.read<ViewBloc>().add(ViewEvent.rename(name));
|
||||
}
|
||||
context.pop();
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export 'mobile_board_screen.dart';
|
||||
export 'mobile_board_content.dart';
|
||||
export 'widgets/mobile_hidden_groups_column.dart';
|
||||
export 'widgets/mobile_board_trailing.dart';
|
||||
@@ -0,0 +1,222 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/mobile/presentation/database/board/board.dart';
|
||||
import 'package:appflowy/mobile/presentation/database/board/widgets/group_card_header.dart';
|
||||
import 'package:appflowy/mobile/presentation/database/card/card.dart';
|
||||
import 'package:appflowy/plugins/database_view/board/application/board_bloc.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_type_extension.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/card/card.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/card/card_cell_builder.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/card/cells/card_cell.dart';
|
||||
import 'package:appflowy_board/appflowy_board.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:go_router/go_router.dart';
|
||||
|
||||
class MobileBoardContent extends StatefulWidget {
|
||||
const MobileBoardContent({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
State<MobileBoardContent> createState() => _MobileBoardContentState();
|
||||
}
|
||||
|
||||
class _MobileBoardContentState extends State<MobileBoardContent> {
|
||||
final renderHook = RowCardRenderHook<String>();
|
||||
late final ScrollController scrollController;
|
||||
late final AppFlowyBoardScrollController scrollManager;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
//mobile may not need this
|
||||
//scroll to bottom when add a new card
|
||||
scrollManager = AppFlowyBoardScrollController();
|
||||
scrollController = ScrollController();
|
||||
renderHook.addSelectOptionHook((options, groupId, _) {
|
||||
// The cell should hide if the option id is equal to the groupId.
|
||||
final isInGroup =
|
||||
options.where((element) => element.id == groupId).isNotEmpty;
|
||||
|
||||
if (isInGroup || options.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final screenWidth = MediaQuery.of(context).size.width;
|
||||
final config = AppFlowyBoardConfig(
|
||||
groupCornerRadius: 8,
|
||||
groupBackgroundColor: Theme.of(context).colorScheme.secondary,
|
||||
groupMargin: const EdgeInsets.fromLTRB(4, 8, 4, 12),
|
||||
groupHeaderPadding: const EdgeInsets.all(8),
|
||||
groupBodyPadding: const EdgeInsets.all(4),
|
||||
groupFooterPadding: const EdgeInsets.all(8),
|
||||
cardMargin: const EdgeInsets.all(4),
|
||||
);
|
||||
|
||||
return BlocListener<BoardBloc, BoardState>(
|
||||
listenWhen: (previous, current) =>
|
||||
previous.recentAddedRowMeta != current.recentAddedRowMeta,
|
||||
listener: (context, state) {
|
||||
context.push(
|
||||
MobileRowDetailPage.routeName,
|
||||
extra: {
|
||||
MobileRowDetailPage.argRowId: state.recentAddedRowMeta!.id,
|
||||
MobileRowDetailPage.argDatabaseController:
|
||||
context.read<BoardBloc>().databaseController,
|
||||
},
|
||||
);
|
||||
},
|
||||
child: BlocBuilder<BoardBloc, BoardState>(
|
||||
builder: (context, state) {
|
||||
final showCreateGroupButton =
|
||||
context.read<BoardBloc>().groupingFieldType.canCreateNewGroup;
|
||||
final showHiddenGroups = state.hiddenGroups.isNotEmpty;
|
||||
return AppFlowyBoard(
|
||||
boardScrollController: scrollManager,
|
||||
scrollController: scrollController,
|
||||
controller: context.read<BoardBloc>().boardController,
|
||||
groupConstraints: BoxConstraints.tightFor(width: screenWidth * 0.7),
|
||||
config: config,
|
||||
leading: showHiddenGroups
|
||||
? MobileHiddenGroupsColumn(padding: config.groupHeaderPadding)
|
||||
: const HSpace(16),
|
||||
trailing: showCreateGroupButton
|
||||
? const MobileBoardTrailing()
|
||||
: const HSpace(16),
|
||||
headerBuilder: (_, groupData) => BlocProvider<BoardBloc>.value(
|
||||
value: context.read<BoardBloc>(),
|
||||
child: GroupCardHeader(
|
||||
groupData: groupData,
|
||||
),
|
||||
),
|
||||
footerBuilder: _buildFooter,
|
||||
cardBuilder: (_, column, columnItem) => _buildCard(
|
||||
context: context,
|
||||
afGroupData: column,
|
||||
afGroupItem: columnItem,
|
||||
cardMargin: config.cardMargin,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFooter(BuildContext context, AppFlowyGroupData columnData) {
|
||||
final style = Theme.of(context);
|
||||
|
||||
return SizedBox(
|
||||
height: 42,
|
||||
width: double.infinity,
|
||||
child: TextButton.icon(
|
||||
style: TextButton.styleFrom(
|
||||
padding: const EdgeInsets.only(left: 8),
|
||||
alignment: Alignment.centerLeft,
|
||||
),
|
||||
icon: FlowySvg(
|
||||
FlowySvgs.add_m,
|
||||
color: style.colorScheme.onSurface,
|
||||
),
|
||||
label: Text(
|
||||
LocaleKeys.board_column_createNewCard.tr(),
|
||||
style: style.textTheme.bodyMedium?.copyWith(
|
||||
color: style.colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
onPressed: () => context
|
||||
.read<BoardBloc>()
|
||||
.add(BoardEvent.createBottomRow(columnData.id)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCard({
|
||||
required BuildContext context,
|
||||
required AppFlowyGroupData afGroupData,
|
||||
required AppFlowyGroupItem afGroupItem,
|
||||
required EdgeInsets cardMargin,
|
||||
}) {
|
||||
final boardBloc = context.read<BoardBloc>();
|
||||
final groupItem = afGroupItem as GroupItem;
|
||||
final groupData = afGroupData.customData as GroupData;
|
||||
final rowMeta = groupItem.row;
|
||||
final rowCache = boardBloc.getRowCache();
|
||||
|
||||
/// Return placeholder widget if the rowCache is null.
|
||||
if (rowCache == null) return SizedBox.shrink(key: ObjectKey(groupItem));
|
||||
final cellCache = rowCache.cellCache;
|
||||
final viewId = boardBloc.viewId;
|
||||
|
||||
final cellBuilder = CardCellBuilder<String>(cellCache);
|
||||
final isEditing = boardBloc.state.isEditingRow &&
|
||||
boardBloc.state.editingRow?.row.id == groupItem.row.id;
|
||||
|
||||
final groupItemId = groupItem.row.id + groupData.group.groupId;
|
||||
|
||||
return Container(
|
||||
key: ValueKey(groupItemId),
|
||||
margin: cardMargin,
|
||||
decoration: _makeBoxDecoration(context),
|
||||
child: RowCard<String>(
|
||||
rowMeta: rowMeta,
|
||||
viewId: viewId,
|
||||
rowCache: rowCache,
|
||||
cardData: groupData.group.groupId,
|
||||
groupingFieldId: groupItem.fieldInfo.id,
|
||||
groupId: groupData.group.groupId,
|
||||
isEditing: isEditing,
|
||||
cellBuilder: cellBuilder,
|
||||
renderHook: renderHook,
|
||||
openCard: (context) {
|
||||
context.push(
|
||||
MobileRowDetailPage.routeName,
|
||||
extra: {
|
||||
MobileRowDetailPage.argRowId: rowMeta.id,
|
||||
MobileRowDetailPage.argDatabaseController:
|
||||
context.read<BoardBloc>().databaseController,
|
||||
},
|
||||
);
|
||||
},
|
||||
onStartEditing: () => boardBloc
|
||||
.add(BoardEvent.startEditingRow(groupData.group, groupItem.row)),
|
||||
onEndEditing: () =>
|
||||
boardBloc.add(BoardEvent.endEditingRow(groupItem.row.id)),
|
||||
styleConfiguration: const RowCardStyleConfiguration(
|
||||
showAccessory: false,
|
||||
hoverStyle: null,
|
||||
cardPadding: EdgeInsets.all(8),
|
||||
cellPadding: EdgeInsets.zero,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
BoxDecoration _makeBoxDecoration(BuildContext context) {
|
||||
return BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
border: Border.fromBorderSide(
|
||||
BorderSide(
|
||||
color: Theme.of(context).colorScheme.outline.withOpacity(0.5),
|
||||
),
|
||||
),
|
||||
boxShadow: [
|
||||
// card shadow
|
||||
BoxShadow(
|
||||
color: Theme.of(context).colorScheme.outline.withOpacity(0.5),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
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_action_widget.dart';
|
||||
import 'package:appflowy/mobile/presentation/widgets/show_flowy_mobile_bottom_sheet.dart';
|
||||
import 'package:appflowy/plugins/database_view/board/application/board_bloc.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_type_extension.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/group.pb.dart';
|
||||
import 'package:appflowy_board/appflowy_board.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:go_router/go_router.dart';
|
||||
|
||||
// similar to [BoardColumnHeader] in Desktop
|
||||
class GroupCardHeader extends StatefulWidget {
|
||||
const GroupCardHeader({
|
||||
super.key,
|
||||
required this.groupData,
|
||||
});
|
||||
|
||||
final AppFlowyGroupData groupData;
|
||||
|
||||
@override
|
||||
State<GroupCardHeader> createState() => _GroupCardHeaderState();
|
||||
}
|
||||
|
||||
class _GroupCardHeaderState extends State<GroupCardHeader> {
|
||||
late final TextEditingController _controller =
|
||||
TextEditingController.fromValue(
|
||||
TextEditingValue(
|
||||
selection: TextSelection.collapsed(
|
||||
offset: widget.groupData.headerData.groupName.length,
|
||||
),
|
||||
text: widget.groupData.headerData.groupName,
|
||||
),
|
||||
);
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final boardCustomData = widget.groupData.customData as GroupData;
|
||||
final titleTextStyle = Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
);
|
||||
return BlocBuilder<BoardBloc, BoardState>(
|
||||
builder: (context, state) {
|
||||
Widget title = Text(
|
||||
widget.groupData.headerData.groupName,
|
||||
style: titleTextStyle,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
);
|
||||
|
||||
// header can be edited if it's not default group(no status) and the field type can be edited
|
||||
if (!boardCustomData.group.isDefault &&
|
||||
boardCustomData.fieldType.canEditHeader) {
|
||||
title = GestureDetector(
|
||||
onTap: () => context
|
||||
.read<BoardBloc>()
|
||||
.add(BoardEvent.startEditingHeader(widget.groupData.id)),
|
||||
child: Text(
|
||||
widget.groupData.headerData.groupName,
|
||||
style: titleTextStyle,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (state.isEditingHeader &&
|
||||
state.editingHeaderId == widget.groupData.id) {
|
||||
title = TextField(
|
||||
controller: _controller,
|
||||
autofocus: true,
|
||||
onEditingComplete: () => context.read<BoardBloc>().add(
|
||||
BoardEvent.endEditingHeader(
|
||||
widget.groupData.id,
|
||||
_controller.text,
|
||||
),
|
||||
),
|
||||
maxLines: 1,
|
||||
style: titleTextStyle,
|
||||
);
|
||||
}
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(left: 16),
|
||||
child: SizedBox(
|
||||
height: 42,
|
||||
child: Row(
|
||||
children: [
|
||||
_buildHeaderIcon(boardCustomData),
|
||||
Expanded(child: title),
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
Icons.more_horiz_rounded,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
splashRadius: 5,
|
||||
onPressed: () => showFlowyMobileBottomSheet(
|
||||
context,
|
||||
title: LocaleKeys.board_column_groupActions.tr(),
|
||||
builder: (_) {
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: BottomSheetActionWidget(
|
||||
svg: FlowySvgs.edit_s,
|
||||
text: LocaleKeys.board_column_renameColumn.tr(),
|
||||
onTap: () {
|
||||
context.read<BoardBloc>().add(
|
||||
BoardEvent.startEditingHeader(
|
||||
widget.groupData.id,
|
||||
),
|
||||
);
|
||||
context.pop();
|
||||
},
|
||||
),
|
||||
),
|
||||
const HSpace(8),
|
||||
Expanded(
|
||||
child: BottomSheetActionWidget(
|
||||
svg: FlowySvgs.hide_s,
|
||||
text: LocaleKeys.board_column_hideColumn.tr(),
|
||||
onTap: () {
|
||||
context.read<BoardBloc>().add(
|
||||
BoardEvent.toggleGroupVisibility(
|
||||
widget.groupData.customData.group
|
||||
as GroupPB,
|
||||
false,
|
||||
),
|
||||
);
|
||||
context.pop();
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
Icons.add,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
splashRadius: 5,
|
||||
onPressed: () => context.read<BoardBloc>().add(
|
||||
BoardEvent.createHeaderRow(widget.groupData.id),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeaderIcon(GroupData customData) =>
|
||||
switch (customData.fieldType) {
|
||||
FieldType.Checkbox => FlowySvg(
|
||||
customData.asCheckboxGroup()!.isCheck
|
||||
? FlowySvgs.check_filled_s
|
||||
: FlowySvgs.uncheck_s,
|
||||
blendMode: BlendMode.dst,
|
||||
),
|
||||
_ => const SizedBox.shrink(),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/database_view/board/application/board_bloc.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
/// Add new group
|
||||
class MobileBoardTrailing extends StatefulWidget {
|
||||
const MobileBoardTrailing({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
State<MobileBoardTrailing> createState() => _MobileBoardTrailingState();
|
||||
}
|
||||
|
||||
class _MobileBoardTrailingState extends State<MobileBoardTrailing> {
|
||||
final TextEditingController _textController = TextEditingController();
|
||||
|
||||
bool isEditing = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final screenSize = MediaQuery.of(context).size;
|
||||
final style = Theme.of(context);
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.all(8),
|
||||
child: SizedBox(
|
||||
width: screenSize.width * 0.7,
|
||||
child: isEditing
|
||||
? DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: style.colorScheme.secondary,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Padding(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
TextField(
|
||||
controller: _textController,
|
||||
autofocus: true,
|
||||
decoration: InputDecoration(
|
||||
suffixIcon: IconButton(
|
||||
icon: Icon(
|
||||
Icons.close,
|
||||
color: style.colorScheme.onBackground,
|
||||
),
|
||||
onPressed: () => _textController.clear(),
|
||||
),
|
||||
isDense: true,
|
||||
),
|
||||
onEditingComplete: () {
|
||||
context.read<BoardBloc>().add(
|
||||
BoardEvent.createGroup(
|
||||
_textController.text,
|
||||
),
|
||||
);
|
||||
_textController.clear();
|
||||
setState(() {
|
||||
isEditing = false;
|
||||
});
|
||||
},
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
TextButton(
|
||||
child: Text(
|
||||
LocaleKeys.button_cancel.tr(),
|
||||
style: style.textTheme.titleSmall?.copyWith(
|
||||
color: style.colorScheme.onBackground,
|
||||
),
|
||||
),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
isEditing = false;
|
||||
});
|
||||
},
|
||||
),
|
||||
TextButton(
|
||||
child: Text(
|
||||
LocaleKeys.button_add.tr(),
|
||||
style: style.textTheme.titleSmall?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: style.colorScheme.onBackground,
|
||||
),
|
||||
),
|
||||
onPressed: () {
|
||||
context.read<BoardBloc>().add(
|
||||
BoardEvent.createGroup(
|
||||
_textController.text,
|
||||
),
|
||||
);
|
||||
_textController.clear();
|
||||
setState(() {
|
||||
isEditing = false;
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
: ElevatedButton.icon(
|
||||
style: ElevatedButton.styleFrom(
|
||||
foregroundColor: style.colorScheme.onBackground,
|
||||
backgroundColor: style.colorScheme.secondary,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
icon: const Icon(Icons.add),
|
||||
label: Text(
|
||||
LocaleKeys.board_column_newGroup.tr(),
|
||||
),
|
||||
onPressed: () => setState(
|
||||
() => isEditing = true,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,316 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/mobile/presentation/database/card/card.dart';
|
||||
import 'package:appflowy/mobile/presentation/widgets/widgets.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/database_controller.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/field/field_info.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/row/row_cache.dart';
|
||||
import 'package:appflowy/plugins/database_view/board/application/board_bloc.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/card/card_cell_builder.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/card/cells/card_cell.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/row/cells/text_cell/text_cell_bloc.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||
import 'package:collection/collection.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:go_router/go_router.dart';
|
||||
|
||||
class MobileHiddenGroupsColumn extends StatelessWidget {
|
||||
const MobileHiddenGroupsColumn({super.key, required this.padding});
|
||||
|
||||
final EdgeInsets padding;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final databaseController = context.read<BoardBloc>().databaseController;
|
||||
return BlocSelector<BoardBloc, BoardState, BoardLayoutSettingPB?>(
|
||||
selector: (state) => state.layoutSettings,
|
||||
builder: (context, layoutSettings) {
|
||||
if (layoutSettings == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
final isCollapsed = layoutSettings.collapseHiddenGroups;
|
||||
return Container(
|
||||
padding: padding,
|
||||
child: AnimatedSize(
|
||||
alignment: AlignmentDirectional.topStart,
|
||||
curve: Curves.easeOut,
|
||||
duration: const Duration(milliseconds: 150),
|
||||
child: isCollapsed
|
||||
? SizedBox(
|
||||
height: 50,
|
||||
child: _collapseExpandIcon(context, isCollapsed),
|
||||
)
|
||||
: SizedBox(
|
||||
width: 180,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Spacer(),
|
||||
_collapseExpandIcon(context, isCollapsed),
|
||||
],
|
||||
),
|
||||
Text(
|
||||
LocaleKeys.board_hiddenGroupSection_sectionTitle.tr(),
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium
|
||||
?.copyWith(
|
||||
color: Theme.of(context).colorScheme.tertiary,
|
||||
),
|
||||
),
|
||||
const VSpace(8),
|
||||
Expanded(
|
||||
child: MobileHiddenGroupList(
|
||||
databaseController: databaseController,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _collapseExpandIcon(BuildContext context, bool isCollapsed) {
|
||||
return CircleAvatar(
|
||||
radius: 20,
|
||||
backgroundColor: Theme.of(context).colorScheme.secondary,
|
||||
child: IconButton(
|
||||
icon: FlowySvg(
|
||||
isCollapsed
|
||||
? FlowySvgs.hamburger_s_s
|
||||
: FlowySvgs.pull_left_outlined_s,
|
||||
size: isCollapsed ? const Size.square(12) : const Size.square(40),
|
||||
),
|
||||
onPressed: () => context
|
||||
.read<BoardBloc>()
|
||||
.add(BoardEvent.toggleHiddenSectionVisibility(!isCollapsed)),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MobileHiddenGroupList extends StatelessWidget {
|
||||
const MobileHiddenGroupList({
|
||||
super.key,
|
||||
required this.databaseController,
|
||||
});
|
||||
|
||||
final DatabaseController databaseController;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final bloc = context.read<BoardBloc>();
|
||||
return BlocBuilder<BoardBloc, BoardState>(
|
||||
builder: (_, state) => ReorderableListView.builder(
|
||||
itemCount: state.hiddenGroups.length,
|
||||
itemBuilder: (_, index) => MobileHiddenGroup(
|
||||
key: ValueKey(state.hiddenGroups[index].groupId),
|
||||
group: state.hiddenGroups[index],
|
||||
index: index,
|
||||
bloc: bloc,
|
||||
),
|
||||
physics: const ClampingScrollPhysics(),
|
||||
onReorder: (oldIndex, newIndex) {
|
||||
if (oldIndex < newIndex) {
|
||||
newIndex--;
|
||||
}
|
||||
final fromGroupId = state.hiddenGroups[oldIndex].groupId;
|
||||
final toGroupId = state.hiddenGroups[newIndex].groupId;
|
||||
bloc.add(BoardEvent.reorderGroup(fromGroupId, toGroupId));
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MobileHiddenGroup extends StatelessWidget {
|
||||
const MobileHiddenGroup({
|
||||
super.key,
|
||||
required this.group,
|
||||
required this.index,
|
||||
required this.bloc,
|
||||
});
|
||||
|
||||
final GroupPB group;
|
||||
final BoardBloc bloc;
|
||||
final int index;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final databaseController = bloc.databaseController;
|
||||
final primaryField = databaseController.fieldController.fieldInfos
|
||||
.firstWhereOrNull((element) => element.isPrimary)!;
|
||||
|
||||
return BlocProvider<BoardBloc>.value(
|
||||
value: bloc,
|
||||
child: BlocBuilder<BoardBloc, BoardState>(
|
||||
builder: (context, state) {
|
||||
final group = state.hiddenGroups.firstWhereOrNull(
|
||||
(g) => g.groupId == this.group.groupId,
|
||||
);
|
||||
if (group == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return ExpansionTile(
|
||||
tilePadding: EdgeInsets.zero,
|
||||
childrenPadding: EdgeInsets.zero,
|
||||
title: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
group.groupName,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
GestureDetector(
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.all(4),
|
||||
child: FlowySvg(
|
||||
FlowySvgs.hide_m,
|
||||
size: Size.square(20),
|
||||
),
|
||||
),
|
||||
onTap: () => showFlowyMobileConfirmDialog(
|
||||
context,
|
||||
title: LocaleKeys.board_mobile_unhideGroup.tr(),
|
||||
content: LocaleKeys.board_mobile_unhideGroupContent.tr(),
|
||||
actionButtonTitle: LocaleKeys.button_yes.tr(),
|
||||
actionButtonColor: Theme.of(context).colorScheme.primary,
|
||||
onActionButtonPressed: () => context.read<BoardBloc>().add(
|
||||
BoardEvent.toggleGroupVisibility(
|
||||
group,
|
||||
true,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
children: [
|
||||
MobileHiddenGroupItemList(
|
||||
bloc: bloc,
|
||||
viewId: databaseController.viewId,
|
||||
groupId: group.groupId,
|
||||
primaryField: primaryField,
|
||||
rowCache: databaseController.rowCache,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MobileHiddenGroupItemList extends StatelessWidget {
|
||||
const MobileHiddenGroupItemList({
|
||||
required this.bloc,
|
||||
required this.groupId,
|
||||
required this.viewId,
|
||||
required this.primaryField,
|
||||
required this.rowCache,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final BoardBloc bloc;
|
||||
final String groupId;
|
||||
final String viewId;
|
||||
final FieldInfo primaryField;
|
||||
final RowCache rowCache;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider.value(
|
||||
value: bloc,
|
||||
child: BlocBuilder<BoardBloc, BoardState>(
|
||||
builder: (context, state) {
|
||||
final group = state.hiddenGroups.firstWhereOrNull(
|
||||
(g) => g.groupId == groupId,
|
||||
);
|
||||
if (group == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
final cells = <Widget>[
|
||||
...group.rows.map(
|
||||
(item) {
|
||||
final cellContext = rowCache.loadCells(item)[primaryField.id]!;
|
||||
final renderHook = RowCardRenderHook<String>();
|
||||
renderHook.addTextCellHook((cellData, _, __) {
|
||||
return BlocBuilder<TextCellBloc, TextCellState>(
|
||||
builder: (context, state) {
|
||||
final text = cellData.isEmpty
|
||||
? LocaleKeys.grid_row_titlePlaceholder.tr()
|
||||
: cellData;
|
||||
|
||||
if (text.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return Row(
|
||||
children: [
|
||||
if (!cellContext.rowMeta.isDocumentEmpty) ...[
|
||||
const FlowySvg(FlowySvgs.notes_s),
|
||||
const HSpace(4),
|
||||
],
|
||||
Expanded(
|
||||
child: Text(
|
||||
text,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
return TextButton(
|
||||
style: TextButton.styleFrom(
|
||||
textStyle: Theme.of(context).textTheme.bodyMedium,
|
||||
foregroundColor: Theme.of(context).colorScheme.onBackground,
|
||||
visualDensity: VisualDensity.compact,
|
||||
),
|
||||
child: CardCellBuilder<String>(rowCache.cellCache).buildCell(
|
||||
cellContext: cellContext,
|
||||
renderHook: renderHook,
|
||||
hasNotes: !cellContext.rowMeta.isDocumentEmpty,
|
||||
),
|
||||
onPressed: () {
|
||||
context.push(
|
||||
MobileRowDetailPage.routeName,
|
||||
extra: {
|
||||
MobileRowDetailPage.argRowId: item.id,
|
||||
MobileRowDetailPage.argDatabaseController:
|
||||
context.read<BoardBloc>().databaseController,
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
];
|
||||
|
||||
return ListView.builder(
|
||||
itemBuilder: (context, index) => cells[index],
|
||||
itemCount: cells.length,
|
||||
shrinkWrap: true,
|
||||
physics: const ClampingScrollPhysics(),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export 'group_card_header.dart';
|
||||
export 'mobile_board_trailing.dart';
|
||||
export 'mobile_hidden_groups_column.dart';
|
||||
@@ -0,0 +1,2 @@
|
||||
export 'card_detail/mobile_card_detail_screen.dart';
|
||||
export 'card_content/mobile_card_content.dart';
|
||||
@@ -0,0 +1,9 @@
|
||||
export 'checkbox.dart';
|
||||
export 'date.dart';
|
||||
export 'style.dart';
|
||||
export 'time_stamp.dart';
|
||||
export 'number.dart';
|
||||
export 'text.dart';
|
||||
export 'select_option.dart';
|
||||
export 'url.dart';
|
||||
export 'checklist.dart';
|
||||
@@ -0,0 +1,68 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/card/cells/card_cell.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/row/cells/checkbox_cell/checkbox_cell_bloc.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class MobileCheckboxCardCell extends CardCell {
|
||||
const MobileCheckboxCardCell({
|
||||
super.key,
|
||||
required this.cellControllerBuilder,
|
||||
});
|
||||
|
||||
final CellControllerBuilder cellControllerBuilder;
|
||||
|
||||
@override
|
||||
State<MobileCheckboxCardCell> createState() => _CheckboxCellState();
|
||||
}
|
||||
|
||||
class _CheckboxCellState extends State<MobileCheckboxCardCell> {
|
||||
late final CheckboxCellBloc _cellBloc;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final cellController =
|
||||
widget.cellControllerBuilder.build() as CheckboxCellController;
|
||||
_cellBloc = CheckboxCellBloc(cellController: cellController)
|
||||
..add(const CheckboxCellEvent.initial());
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> dispose() async {
|
||||
_cellBloc.close();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider.value(
|
||||
value: _cellBloc,
|
||||
child: BlocBuilder<CheckboxCellBloc, CheckboxCellState>(
|
||||
buildWhen: (previous, current) =>
|
||||
previous.isSelected != current.isSelected,
|
||||
builder: (context, state) {
|
||||
return Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: IconButton(
|
||||
padding: EdgeInsets.zero,
|
||||
alignment: Alignment.centerLeft,
|
||||
visualDensity: VisualDensity.compact,
|
||||
icon: FlowySvg(
|
||||
state.isSelected
|
||||
? FlowySvgs.check_filled_s
|
||||
: FlowySvgs.uncheck_s,
|
||||
blendMode: BlendMode.dst,
|
||||
size: const Size.square(24),
|
||||
),
|
||||
onPressed: () => context
|
||||
.read<CheckboxCellBloc>()
|
||||
.add(const CheckboxCellEvent.select()),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
import 'package:appflowy/mobile/presentation/database/card/card_content/card_cells/card_cells.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/card/cells/card_cell.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/row/cells/checklist_cell/checklist_cell_bloc.dart';
|
||||
import 'package:flowy_infra/theme_extension.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:percent_indicator/linear_percent_indicator.dart';
|
||||
|
||||
class MobileChecklistCardCell extends CardCell {
|
||||
const MobileChecklistCardCell({
|
||||
super.key,
|
||||
required this.cellControllerBuilder,
|
||||
});
|
||||
|
||||
final CellControllerBuilder cellControllerBuilder;
|
||||
|
||||
@override
|
||||
State<MobileChecklistCardCell> createState() => _ChecklistCellState();
|
||||
}
|
||||
|
||||
class _ChecklistCellState extends State<MobileChecklistCardCell> {
|
||||
late final ChecklistCellBloc _cellBloc;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final cellController =
|
||||
widget.cellControllerBuilder.build() as ChecklistCellController;
|
||||
_cellBloc = ChecklistCellBloc(cellController: cellController)
|
||||
..add(const ChecklistCellEvent.initial());
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_cellBloc.close();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cellStyle = MobileCardCellStyle(context);
|
||||
return BlocProvider.value(
|
||||
value: _cellBloc,
|
||||
child: BlocBuilder<ChecklistCellBloc, ChecklistCellState>(
|
||||
builder: (context, state) {
|
||||
if (state.tasks.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
return Padding(
|
||||
padding: cellStyle.padding,
|
||||
child: MobileChecklistProgressBar(
|
||||
tasks: state.tasks,
|
||||
percent: state.percent,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MobileChecklistProgressBar extends StatefulWidget {
|
||||
const MobileChecklistProgressBar({
|
||||
super.key,
|
||||
required this.tasks,
|
||||
required this.percent,
|
||||
});
|
||||
|
||||
final List<ChecklistSelectOption> tasks;
|
||||
final double percent;
|
||||
final int segmentLimit = 5;
|
||||
|
||||
@override
|
||||
State<MobileChecklistProgressBar> createState() =>
|
||||
_MobileChecklistProgresssBarState();
|
||||
}
|
||||
|
||||
class _MobileChecklistProgresssBarState
|
||||
extends State<MobileChecklistProgressBar> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cellStyle = MobileCardCellStyle(context);
|
||||
final numFinishedTasks = widget.tasks.where((e) => e.isSelected).length;
|
||||
final completedTaskColor = numFinishedTasks == widget.tasks.length
|
||||
? AFThemeExtension.of(context).success
|
||||
: Theme.of(context).colorScheme.primary;
|
||||
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Row(
|
||||
children: [
|
||||
if (widget.tasks.isNotEmpty &&
|
||||
widget.tasks.length <= widget.segmentLimit)
|
||||
...List<Widget>.generate(
|
||||
widget.tasks.length,
|
||||
(index) => Flexible(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius:
|
||||
const BorderRadius.all(Radius.circular(2)),
|
||||
color: index < numFinishedTasks
|
||||
? completedTaskColor
|
||||
: AFThemeExtension.of(context).progressBarBGColor,
|
||||
),
|
||||
margin: const EdgeInsets.symmetric(horizontal: 1),
|
||||
height: 6.0,
|
||||
),
|
||||
),
|
||||
)
|
||||
else
|
||||
Expanded(
|
||||
child: LinearPercentIndicator(
|
||||
lineHeight: 6.0,
|
||||
percent: widget.percent,
|
||||
padding: EdgeInsets.zero,
|
||||
progressColor: completedTaskColor,
|
||||
backgroundColor:
|
||||
AFThemeExtension.of(context).progressBarBGColor,
|
||||
barRadius: const Radius.circular(2),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 8),
|
||||
child: Text(
|
||||
"${(widget.percent * 100).round()}%",
|
||||
style: cellStyle.secondaryTextStyle(),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import 'package:appflowy/mobile/presentation/database/card/card_content/card_cells/style.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/card/cells/card_cell.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/row/cells/date_cell/date_cell_bloc.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class MobileDateCardCell<CustomCardData> extends CardCell {
|
||||
const MobileDateCardCell({
|
||||
super.key,
|
||||
required this.cellControllerBuilder,
|
||||
this.renderHook,
|
||||
});
|
||||
|
||||
final CellControllerBuilder cellControllerBuilder;
|
||||
final CellRenderHook<dynamic, CustomCardData>? renderHook;
|
||||
|
||||
@override
|
||||
State<MobileDateCardCell> createState() => _DateCellState();
|
||||
}
|
||||
|
||||
class _DateCellState extends State<MobileDateCardCell> {
|
||||
late final DateCellBloc _cellBloc;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final cellController =
|
||||
widget.cellControllerBuilder.build() as DateCellController;
|
||||
|
||||
_cellBloc = DateCellBloc(cellController: cellController)
|
||||
..add(const DateCellEvent.initial());
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> dispose() async {
|
||||
_cellBloc.close();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cellStyle = MobileCardCellStyle(context);
|
||||
return BlocProvider.value(
|
||||
value: _cellBloc,
|
||||
child: BlocBuilder<DateCellBloc, DateCellState>(
|
||||
buildWhen: (previous, current) => previous.dateStr != current.dateStr,
|
||||
builder: (context, state) {
|
||||
if (state.dateStr.isEmpty) {
|
||||
return const SizedBox();
|
||||
} else {
|
||||
final Widget? custom = widget.renderHook?.call(
|
||||
state.data,
|
||||
widget.cardData,
|
||||
context,
|
||||
);
|
||||
if (custom != null) {
|
||||
return custom;
|
||||
}
|
||||
|
||||
return Container(
|
||||
alignment: Alignment.centerLeft,
|
||||
padding: cellStyle.padding,
|
||||
child: Text(
|
||||
state.dateStr,
|
||||
style: cellStyle.secondaryTextStyle(),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||