Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e81a2ff577 | ||
|
|
b356927e60 | ||
|
|
1894409dc3 | ||
|
|
f29f1bae32 | ||
|
|
45054e7639 | ||
|
|
3b51a6e6be | ||
|
|
747fe40648 | ||
|
|
60fc5bb2c8 | ||
|
|
9d71464f1a | ||
|
|
92ceb2cd7d | ||
|
|
0db21b91a3 | ||
|
|
88e9d63a13 | ||
|
|
3129fa6cc1 | ||
|
|
db5372c18e | ||
|
|
b781c9aa0f | ||
|
|
a9c6b80f4a | ||
|
|
271f781cce | ||
|
|
ef4bce25d8 | ||
|
|
95c4cb241a | ||
|
|
e20d938ce3 | ||
|
|
a515715543 | ||
|
|
fda70ff560 | ||
|
|
08938b8c70 | ||
|
|
250f29f325 | ||
|
|
d117cd6b5b | ||
|
|
ca93cb20ec | ||
|
|
8c1d0106dd | ||
|
|
5cbc8b1e18 | ||
|
|
274742e334 | ||
|
|
792f56db79 | ||
|
|
458d3f971b | ||
|
|
2ffd6bf7ea | ||
|
|
722691c922 | ||
|
|
9746852b5f | ||
|
|
5b030303a6 | ||
|
|
1e4f342fc3 | ||
|
|
4f0379bdd7 |
No files matched your search
@@ -1,99 +0,0 @@
|
||||
name: Build AppFlowy Release APK
|
||||
|
||||
on: workflow_dispatch
|
||||
|
||||
env:
|
||||
FLUTTER_VERSION: "3.18.0-0.2.pre"
|
||||
RUST_TOOLCHAIN: "1.75"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest]
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
target: aarch64-linux-android
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
steps:
|
||||
# the following step is required to avoid running out of space
|
||||
- name: Maximize build space
|
||||
if: matrix.os == 'ubuntu-latest'
|
||||
run: |
|
||||
sudo rm -rf /usr/share/dotnet
|
||||
sudo rm -rf /opt/ghc
|
||||
sudo rm -rf "/usr/local/share/boost"
|
||||
sudo rm -rf "$AGENT_TOOLSDIRECTORY"
|
||||
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Install Rust toolchain
|
||||
id: rust_toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: ${{ env.RUST_TOOLCHAIN }}
|
||||
target: ${{ matrix.target }}
|
||||
override: true
|
||||
profile: minimal
|
||||
|
||||
- name: Install flutter
|
||||
id: flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
channel: "beta"
|
||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||
cache: true
|
||||
|
||||
- uses: nttld/setup-ndk@v1
|
||||
id: setup-ndk
|
||||
with:
|
||||
ndk-version: "r24"
|
||||
add-to-path: true
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
prefix-key: ${{ matrix.os }}
|
||||
workspaces: |
|
||||
frontend/rust-lib
|
||||
|
||||
- uses: davidB/rust-cargo-make@v1
|
||||
with:
|
||||
version: "0.36.6"
|
||||
|
||||
- name: Install prerequisites
|
||||
working-directory: frontend
|
||||
run: |
|
||||
rustup target install aarch64-linux-android
|
||||
rustup target install x86_64-linux-android
|
||||
cargo install --force duckscript_cli
|
||||
cargo install cargo-ndk
|
||||
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
|
||||
sudo apt-get install keybinder-3.0 libnotify-dev
|
||||
sudo apt-get install gcc-multilib
|
||||
elif [ "$RUNNER_OS" == "Windows" ]; then
|
||||
vcpkg integrate install
|
||||
elif [ "$RUNNER_OS" == "macOS" ]; then
|
||||
echo 'do nothing'
|
||||
fi
|
||||
cargo make appflowy-flutter-deps-tools
|
||||
shell: bash
|
||||
|
||||
- name: Build AppFlowy
|
||||
working-directory: frontend
|
||||
env:
|
||||
ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }}
|
||||
run: |
|
||||
cargo make --profile production-android appflowy-android
|
||||
|
||||
- name: Upload APK
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: app-arm64-v8a-release.apk
|
||||
path: frontend/appflowy_flutter/build/app/outputs/flutter-apk/
|
||||
@@ -0,0 +1,29 @@
|
||||
name: Build Bot
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
|
||||
jobs:
|
||||
dispatch_slash_command:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
# get build name from pubspec.yaml
|
||||
- name: Get build version
|
||||
working-directory: frontend/appflowy_flutter
|
||||
id: get_build_name
|
||||
run: |
|
||||
echo "fetching version from pubspec.yaml..."
|
||||
echo "build_name=$(grep 'version: ' pubspec.yaml | awk '{print $2}')" >> $GITHUB_OUTPUT
|
||||
|
||||
|
||||
- uses: peter-evans/slash-command-dispatch@v4
|
||||
with:
|
||||
token: ${{ secrets.PAT }}
|
||||
commands: build
|
||||
static-args: |
|
||||
ref=refs/pull/${{ github.event.issue.number }}/head
|
||||
build_name=${{ steps.get_build_name.outputs.build_name }}
|
||||
@@ -0,0 +1,42 @@
|
||||
name: build
|
||||
|
||||
on:
|
||||
repository_dispatch:
|
||||
types: [build-command]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: notify appflowy_builder
|
||||
run: |
|
||||
platform=${{ github.event.client_payload.slash_command.args.unnamed.arg1 }}
|
||||
build_name=${{ github.event.client_payload.slash_command.args.named.build_name }}
|
||||
branch=${{ github.event.client_payload.slash_command.args.named.ref }}
|
||||
build_type=""
|
||||
arch=""
|
||||
|
||||
if [ "$platform" = "android" ]; then
|
||||
build_type="apk"
|
||||
elif [ "$platform" = "macos" ]; then
|
||||
arch="universal"
|
||||
fi
|
||||
|
||||
params=$(jq -n \
|
||||
--arg ref "main" \
|
||||
--arg repo "LucasXu0/AppFlowy" \
|
||||
--arg branch "$branch" \
|
||||
--arg build_name "$build_name" \
|
||||
--arg build_type "$build_type" \
|
||||
--arg arch "$arch" \
|
||||
'{ref: $ref, inputs: {repo: $repo, branch: $branch, build_name: $build_name, build_type: $build_type, arch: $arch}} | del(.inputs | .. | select(. == ""))')
|
||||
|
||||
echo "params: $params"
|
||||
|
||||
curl -L \
|
||||
-X POST \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
-H "Authorization: Bearer ${{ secrets.TOKEN }}" \
|
||||
-H "X-GitHub-Api-Version: 2022-11-28" \
|
||||
https://api.github.com/repos/AppFlowy-IO/AppFlowy-Builder/actions/workflows/$platform.yaml/dispatches \
|
||||
-d "$params"
|
||||
@@ -19,7 +19,6 @@ on:
|
||||
|
||||
env:
|
||||
FLUTTER_VERSION: "3.18.0-0.2.pre"
|
||||
RUST_TOOLCHAIN: "1.75"
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
@@ -46,19 +45,14 @@ jobs:
|
||||
sudo rm -rf /opt/ghc
|
||||
sudo rm -rf "/usr/local/share/boost"
|
||||
sudo rm -rf "$AGENT_TOOLSDIRECTORY"
|
||||
sudo docker image prune --all --force
|
||||
sudo rm -rf /opt/hostedtoolcache/codeQL
|
||||
sudo rm -rf ${GITHUB_WORKSPACE}/.git
|
||||
sudo rm -rf $ANDROID_HOME/ndk
|
||||
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Install Rust toolchain
|
||||
id: rust_toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: ${{ env.RUST_TOOLCHAIN }}
|
||||
target: ${{ matrix.target }}
|
||||
override: true
|
||||
profile: minimal
|
||||
|
||||
- name: Install flutter
|
||||
id: flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
@@ -70,8 +64,8 @@ jobs:
|
||||
- uses: nttld/setup-ndk@v1
|
||||
id: setup-ndk
|
||||
with:
|
||||
ndk-version: "r24"
|
||||
add-to-path: true
|
||||
ndk-version: "r24"
|
||||
add-to-path: true
|
||||
|
||||
- uses: gradle/gradle-build-action@v2
|
||||
with:
|
||||
|
||||
@@ -327,6 +327,8 @@ jobs:
|
||||
LINUX_PACKAGE_DEB_NAME: AppFlowy-${{ github.ref_name }}-linux-x86_64.deb
|
||||
LINUX_PACKAGE_RPM_NAME: AppFlowy-${{ github.ref_name }}-linux-x86_64.rpm
|
||||
LINUX_PACKAGE_TMP_RPM_NAME: AppFlowy-${{ github.ref_name }}-2.x86_64.rpm
|
||||
LINUX_PACKAGE_TMP_APPIMAGE_NAME: AppFlowy-${{ github.ref_name }}-x86_64.AppImage
|
||||
LINUX_PACKAGE_APPIMAGE_NAME: AppFlowy-${{ github.ref_name }}-linux-x86_64.AppImage
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
@@ -395,11 +397,15 @@ jobs:
|
||||
- name: Build Linux package (.rpm)
|
||||
working-directory: ${{ env.LINUX_APP_RELEASE_PATH }}
|
||||
run: |
|
||||
ls -a
|
||||
sudo alien -r ${{ env.LINUX_PACKAGE_DEB_NAME }}
|
||||
echo '------------'
|
||||
cp -r ${{ env.LINUX_PACKAGE_TMP_RPM_NAME }} ${{ env.LINUX_PACKAGE_RPM_NAME }}
|
||||
ls -a
|
||||
|
||||
- name: Build Linux package (.AppImage)
|
||||
working-directory: frontend
|
||||
continue-on-error: true
|
||||
run: |
|
||||
sh scripts/linux_distribution/appimage/build_appimage.sh ${{ github.ref_name }}
|
||||
cp -r ${{ env.LINUX_PACKAGE_TMP_APPIMAGE_NAME }} ${{ env.LINUX_PACKAGE_APPIMAGE_NAME }}
|
||||
|
||||
- name: Upload Asset
|
||||
id: upload-release-asset
|
||||
@@ -434,6 +440,17 @@ jobs:
|
||||
asset_name: ${{ env.LINUX_PACKAGE_RPM_NAME }}
|
||||
asset_content_type: application/octet-stream
|
||||
|
||||
- name: Upload AppImage package
|
||||
id: upload-release-asset-install-package-appimage
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ needs.create-release.outputs.upload_url }}
|
||||
asset_path: ${{ env.LINUX_APP_RELEASE_PATH }}/${{ env.LINUX_PACKAGE_APPIMAGE_NAME }}
|
||||
asset_name: ${{ env.LINUX_PACKAGE_APPIMAGE_NAME }}
|
||||
asset_content_type: application/octet-stream
|
||||
|
||||
build-for-docker:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
@@ -1,4 +1,16 @@
|
||||
# Release Notes
|
||||
## Version 0.4.8 - 02/13/2024
|
||||
### Bug Fixes
|
||||
- Fixed a possible error when loading workspaces
|
||||
|
||||
## Version 0.4.6 - 02/03/2024
|
||||
### Bug Fixes
|
||||
- Fixed refresh token bug
|
||||
|
||||
## Version 0.4.5 - 02/01/2024
|
||||
### Bug Fixes
|
||||
- Fixed WebSocket connection issue
|
||||
|
||||
## Version 0.4.4 - 01/31/2024
|
||||
### New Features
|
||||
- Added functionality for uploading images to cloud storage.
|
||||
|
||||
@@ -26,7 +26,7 @@ CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true
|
||||
CARGO_MAKE_CRATE_FS_NAME = "dart_ffi"
|
||||
CARGO_MAKE_CRATE_NAME = "dart-ffi"
|
||||
LIB_NAME = "dart_ffi"
|
||||
APPFLOWY_VERSION = "0.4.4"
|
||||
APPFLOWY_VERSION = "0.4.8"
|
||||
FLUTTER_DESKTOP_FEATURES = "dart,rev-sqlite"
|
||||
PRODUCT_NAME = "AppFlowy"
|
||||
MACOSX_DEPLOYMENT_TARGET = "11.0"
|
||||
@@ -50,6 +50,7 @@ APP_ENVIRONMENT = "local"
|
||||
FLUTTER_FLOWY_SDK_PATH = "appflowy_flutter/packages/appflowy_backend"
|
||||
TAURI_BACKEND_SERVICE_PATH = "appflowy_tauri/src/services/backend"
|
||||
WEB_BACKEND_SERVICE_PATH = "appflowy_web/src/services/backend"
|
||||
WEB_LIB_PATH= "appflowy_web/wasm-libs/af-wasm"
|
||||
# Test default config
|
||||
TEST_CRATE_TYPE = "cdylib"
|
||||
TEST_LIB_EXT = "dylib"
|
||||
|
||||
@@ -39,12 +39,7 @@ void main() {
|
||||
await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);
|
||||
|
||||
// Invoke the field editor
|
||||
await tester.tapGridFieldWithName('Type');
|
||||
await tester.tapEditFieldButton();
|
||||
|
||||
await tester.tapSwitchFieldTypeButton();
|
||||
await tester.selectFieldType(FieldType.Checkbox);
|
||||
await tester.dismissFieldEditor();
|
||||
await tester.changeFieldTypeOfFieldWithName('Type', FieldType.Checkbox);
|
||||
|
||||
await tester.assertFieldTypeWithFieldName(
|
||||
'Type',
|
||||
|
||||
@@ -86,7 +86,7 @@ void main() {
|
||||
|
||||
testWidgets('add checkbox sort', (tester) async {
|
||||
await tester.openV020database();
|
||||
// create a filter
|
||||
// create a sort
|
||||
await tester.tapDatabaseSortButton();
|
||||
await tester.tapCreateSortByFieldType(FieldType.Checkbox, 'Done');
|
||||
|
||||
@@ -136,7 +136,7 @@ void main() {
|
||||
|
||||
testWidgets('add number sort', (tester) async {
|
||||
await tester.openV020database();
|
||||
// create a filter
|
||||
// create a sort
|
||||
await tester.tapDatabaseSortButton();
|
||||
await tester.tapCreateSortByFieldType(FieldType.Number, 'number');
|
||||
|
||||
@@ -188,7 +188,7 @@ void main() {
|
||||
|
||||
testWidgets('add checkbox and number sort', (tester) async {
|
||||
await tester.openV020database();
|
||||
// create a filter
|
||||
// create a sort
|
||||
await tester.tapDatabaseSortButton();
|
||||
await tester.tapCreateSortByFieldType(FieldType.Checkbox, 'Done');
|
||||
|
||||
@@ -264,5 +264,111 @@ void main() {
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
|
||||
testWidgets('reorder sort', (tester) async {
|
||||
await tester.openV020database();
|
||||
// create a sort
|
||||
await tester.tapDatabaseSortButton();
|
||||
await tester.tapCreateSortByFieldType(FieldType.Checkbox, 'Done');
|
||||
|
||||
// open the sort menu and sort checkbox by descending
|
||||
await tester.tapSortMenuInSettingBar();
|
||||
await tester.tapSortButtonByName('Done');
|
||||
await tester.tapSortByDescending();
|
||||
|
||||
// add another sort, this time by number descending
|
||||
await tester.tapSortMenuInSettingBar();
|
||||
await tester.tapCreateSortByFieldTypeInSortMenu(
|
||||
FieldType.Number,
|
||||
'number',
|
||||
);
|
||||
await tester.tapSortButtonByName('number');
|
||||
await tester.tapSortByDescending();
|
||||
|
||||
// check checkbox cell order
|
||||
for (final (index, content) in <bool>[
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
].indexed) {
|
||||
await tester.assertCheckboxCell(
|
||||
rowIndex: index,
|
||||
isSelected: content,
|
||||
);
|
||||
}
|
||||
|
||||
// check number cell order
|
||||
for (final (index, content) in <String>[
|
||||
'1',
|
||||
'0.2',
|
||||
'0.1',
|
||||
'-1',
|
||||
'-2',
|
||||
'12',
|
||||
'11',
|
||||
'10',
|
||||
'2',
|
||||
'',
|
||||
].indexed) {
|
||||
tester.assertCellContent(
|
||||
rowIndex: index,
|
||||
fieldType: FieldType.Number,
|
||||
content: content,
|
||||
);
|
||||
}
|
||||
|
||||
// reorder sort
|
||||
await tester.tapSortMenuInSettingBar();
|
||||
await tester.reorderSort(
|
||||
(FieldType.Number, 'number'),
|
||||
(FieldType.Checkbox, 'Done'),
|
||||
);
|
||||
|
||||
// check checkbox cell order
|
||||
for (final (index, content) in <bool>[
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
].indexed) {
|
||||
await tester.assertCheckboxCell(
|
||||
rowIndex: index,
|
||||
isSelected: content,
|
||||
);
|
||||
}
|
||||
|
||||
// check the number cell order
|
||||
for (final (index, content) in <String>[
|
||||
'12',
|
||||
'11',
|
||||
'10',
|
||||
'2',
|
||||
'1',
|
||||
'0.2',
|
||||
'0.1',
|
||||
'-1',
|
||||
'-2',
|
||||
'',
|
||||
].indexed) {
|
||||
tester.assertCellContent(
|
||||
rowIndex: index,
|
||||
fieldType: FieldType.Number,
|
||||
content: content,
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pbenum.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('Grid Calculations', () {
|
||||
testWidgets('add calculation and update cell', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);
|
||||
|
||||
// Change one Field to Number
|
||||
await tester.changeFieldTypeOfFieldWithName('Type', FieldType.Number);
|
||||
|
||||
expect(find.text('Calculate'), findsOneWidget);
|
||||
|
||||
await tester.changeCalculateAtIndex(1, CalculationType.Sum);
|
||||
|
||||
// Enter values in cells
|
||||
await tester.editCell(
|
||||
rowIndex: 0,
|
||||
fieldType: FieldType.Number,
|
||||
input: '100',
|
||||
);
|
||||
|
||||
await tester.editCell(
|
||||
rowIndex: 1,
|
||||
fieldType: FieldType.Number,
|
||||
input: '100',
|
||||
);
|
||||
|
||||
// Dismiss edit cell
|
||||
await tester.sendKeyDownEvent(LogicalKeyboardKey.enter);
|
||||
|
||||
await tester.pumpAndSettle(const Duration(seconds: 1));
|
||||
|
||||
expect(find.text('200'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('add calculations and remove row', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);
|
||||
|
||||
// Change two Fields to Number
|
||||
await tester.changeFieldTypeOfFieldWithName('Type', FieldType.Number);
|
||||
await tester.changeFieldTypeOfFieldWithName('Done', FieldType.Number);
|
||||
|
||||
expect(find.text('Calculate'), findsNWidgets(2));
|
||||
|
||||
await tester.changeCalculateAtIndex(1, CalculationType.Sum);
|
||||
await tester.changeCalculateAtIndex(2, CalculationType.Min);
|
||||
|
||||
// Enter values in cells
|
||||
await tester.editCell(
|
||||
rowIndex: 0,
|
||||
fieldType: FieldType.Number,
|
||||
input: '100',
|
||||
);
|
||||
await tester.editCell(
|
||||
rowIndex: 1,
|
||||
fieldType: FieldType.Number,
|
||||
input: '150',
|
||||
);
|
||||
await tester.editCell(
|
||||
rowIndex: 0,
|
||||
fieldType: FieldType.Number,
|
||||
input: '50',
|
||||
cellIndex: 1,
|
||||
);
|
||||
await tester.editCell(
|
||||
rowIndex: 1,
|
||||
fieldType: FieldType.Number,
|
||||
input: '100',
|
||||
cellIndex: 1,
|
||||
);
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Dismiss edit cell
|
||||
await tester.sendKeyDownEvent(LogicalKeyboardKey.enter);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('250'), findsOneWidget);
|
||||
expect(find.text('50'), findsNWidgets(2));
|
||||
|
||||
// Delete 1st row
|
||||
await tester.hoverOnFirstRowOfGrid();
|
||||
await tester.tapRowMenuButtonInGrid();
|
||||
await tester.tapDeleteOnRowMenu();
|
||||
|
||||
await tester.pumpAndSettle(const Duration(seconds: 1));
|
||||
|
||||
expect(find.text('150'), findsNWidgets(2));
|
||||
expect(find.text('100'), findsNWidgets(2));
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1,5 +1,8 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:appflowy/plugins/database/application/calculations/calculation_type_ext.dart';
|
||||
import 'package:appflowy/plugins/database/grid/presentation/widgets/calculations/calculate_cell.dart';
|
||||
import 'package:appflowy/plugins/database/grid/presentation/widgets/calculations/calculation_type_item.dart';
|
||||
import 'package:appflowy/plugins/database/grid/presentation/widgets/common/type_option_separator.dart';
|
||||
import 'package:appflowy/plugins/database/grid/presentation/widgets/header/type_option/number.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/checkbox.dart';
|
||||
@@ -720,6 +723,31 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
||||
await pumpAndSettle();
|
||||
}
|
||||
|
||||
Future<void> changeFieldTypeOfFieldWithName(
|
||||
String name,
|
||||
FieldType type,
|
||||
) async {
|
||||
await tapGridFieldWithName(name);
|
||||
await tapEditFieldButton();
|
||||
|
||||
await tapSwitchFieldTypeButton();
|
||||
await selectFieldType(type);
|
||||
await dismissFieldEditor();
|
||||
}
|
||||
|
||||
Future<void> changeCalculateAtIndex(int index, CalculationType type) async {
|
||||
await tap(find.byType(CalculateCell).at(index));
|
||||
await pumpAndSettle();
|
||||
|
||||
await tap(
|
||||
find.descendant(
|
||||
of: find.byType(CalculationTypeItem),
|
||||
matching: find.text(type.label),
|
||||
),
|
||||
);
|
||||
await pumpAndSettle();
|
||||
}
|
||||
|
||||
/// Should call [tapGridFieldWithName] first.
|
||||
Future<void> tapEditFieldButton() async {
|
||||
await tapButtonWithName(LocaleKeys.grid_field_editProperty.tr());
|
||||
@@ -1042,6 +1070,34 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
||||
await tapButton(findSortItem);
|
||||
}
|
||||
|
||||
/// Must call [tapSortMenuInSettingBar] first.
|
||||
Future<void> reorderSort(
|
||||
(FieldType, String) from,
|
||||
(FieldType, String) to,
|
||||
) async {
|
||||
final fromSortItem = find.byWidgetPredicate(
|
||||
(widget) =>
|
||||
widget is DatabaseSortItem &&
|
||||
widget.sortInfo.fieldInfo.fieldType == from.$1 &&
|
||||
widget.sortInfo.fieldInfo.name == from.$2,
|
||||
);
|
||||
final toSortItem = find.byWidgetPredicate(
|
||||
(widget) =>
|
||||
widget is DatabaseSortItem &&
|
||||
widget.sortInfo.fieldInfo.fieldType == to.$1 &&
|
||||
widget.sortInfo.fieldInfo.name == to.$2,
|
||||
);
|
||||
final dragElement = find.descendant(
|
||||
of: fromSortItem,
|
||||
matching: find.byType(ReorderableDragStartListener),
|
||||
);
|
||||
await drag(
|
||||
dragElement,
|
||||
getCenter(toSortItem) - getCenter(fromSortItem),
|
||||
);
|
||||
await pumpAndSettle(const Duration(milliseconds: 200));
|
||||
}
|
||||
|
||||
/// Must call [tapSortButtonByName] first.
|
||||
Future<void> tapSortByDescending() async {
|
||||
await tapButton(
|
||||
|
||||
+13
-4
@@ -54,7 +54,10 @@ Future<AuthenticatorType> getAuthenticatorType() async {
|
||||
final value = await getIt<KeyValueStorage>().get(KVKeys.kCloudType);
|
||||
if (value.isNone() && !integrationMode().isUnitTest) {
|
||||
// if the cloud type is not set, then set it to AppFlowy Cloud as default.
|
||||
await useAppFlowyBetaCloudWithURL(kAppflowyCloudUrl);
|
||||
await useAppFlowyBetaCloudWithURL(
|
||||
kAppflowyCloudUrl,
|
||||
AuthenticatorType.appflowyCloud,
|
||||
);
|
||||
return AuthenticatorType.appflowyCloud;
|
||||
}
|
||||
|
||||
@@ -70,7 +73,10 @@ Future<AuthenticatorType> getAuthenticatorType() async {
|
||||
case "4":
|
||||
return AuthenticatorType.appflowyCloudDevelop;
|
||||
default:
|
||||
await useAppFlowyBetaCloudWithURL(kAppflowyCloudUrl);
|
||||
await useAppFlowyBetaCloudWithURL(
|
||||
kAppflowyCloudUrl,
|
||||
AuthenticatorType.appflowyCloud,
|
||||
);
|
||||
return AuthenticatorType.appflowyCloud;
|
||||
}
|
||||
}
|
||||
@@ -183,8 +189,11 @@ Future<void> useSelfHostedAppFlowyCloudWithURL(String url) async {
|
||||
await _setAppFlowyCloudUrl(Some(url));
|
||||
}
|
||||
|
||||
Future<void> useAppFlowyBetaCloudWithURL(String url) async {
|
||||
await _setAuthenticatorType(AuthenticatorType.appflowyCloud);
|
||||
Future<void> useAppFlowyBetaCloudWithURL(
|
||||
String url,
|
||||
AuthenticatorType authenticatorType,
|
||||
) async {
|
||||
await _setAuthenticatorType(authenticatorType);
|
||||
await _setAppFlowyCloudUrl(Some(url));
|
||||
}
|
||||
|
||||
|
||||
@@ -144,8 +144,6 @@ class AppBarButton extends StatelessWidget {
|
||||
required this.child,
|
||||
});
|
||||
|
||||
static const defaultWidth = 40.0;
|
||||
|
||||
final VoidCallback onTap;
|
||||
final Widget child;
|
||||
final bool isActionButton;
|
||||
@@ -153,6 +151,7 @@ class AppBarButton extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: onTap,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(
|
||||
|
||||
-1
@@ -44,7 +44,6 @@ class _MobileBottomSheetEditLinkWidgetState
|
||||
void dispose() {
|
||||
textController.dispose();
|
||||
hrefController.dispose();
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
||||
+5
-3
@@ -27,14 +27,16 @@ class _MobileBottomSheetRenameWidgetState
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
controller = TextEditingController(text: widget.name);
|
||||
controller = TextEditingController(text: widget.name)
|
||||
..selection = TextSelection(
|
||||
baseOffset: 0,
|
||||
extentOffset: widget.name.length,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
controller.dispose();
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
||||
+19
-27
@@ -1,7 +1,8 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/mobile/presentation/widgets/widgets.dart';
|
||||
import 'package:appflowy/mobile/presentation/widgets/flowy_mobile_quick_action_button.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/widget/separated_flex.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
enum MobileViewItemBottomSheetBodyAction {
|
||||
@@ -25,55 +26,46 @@ class MobileViewItemBottomSheetBody extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
return SeparatedColumn(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
separatorBuilder: () => const Divider(
|
||||
height: 8.5,
|
||||
thickness: 0.5,
|
||||
),
|
||||
children: [
|
||||
FlowyOptionTile.text(
|
||||
MobileQuickActionButton(
|
||||
text: LocaleKeys.button_rename.tr(),
|
||||
leftIcon: const FlowySvg(
|
||||
FlowySvgs.m_rename_s,
|
||||
),
|
||||
showTopBorder: false,
|
||||
icon: FlowySvgs.m_rename_s,
|
||||
onTap: () => onAction(
|
||||
MobileViewItemBottomSheetBodyAction.rename,
|
||||
),
|
||||
),
|
||||
FlowyOptionTile.text(
|
||||
MobileQuickActionButton(
|
||||
text: isFavorite
|
||||
? LocaleKeys.button_removeFromFavorites.tr()
|
||||
: LocaleKeys.button_addToFavorites.tr(),
|
||||
leftIcon: FlowySvg(
|
||||
size: const Size(20, 20),
|
||||
isFavorite
|
||||
? FlowySvgs.m_favorite_selected_lg
|
||||
: FlowySvgs.m_favorite_unselected_lg,
|
||||
color: isFavorite ? Colors.yellow : null,
|
||||
),
|
||||
showTopBorder: false,
|
||||
icon: isFavorite
|
||||
? FlowySvgs.m_favorite_selected_lg
|
||||
: FlowySvgs.m_favorite_unselected_lg,
|
||||
iconColor: isFavorite ? Colors.yellow : null,
|
||||
onTap: () => onAction(
|
||||
isFavorite
|
||||
? MobileViewItemBottomSheetBodyAction.removeFromFavorites
|
||||
: MobileViewItemBottomSheetBodyAction.addToFavorites,
|
||||
),
|
||||
),
|
||||
FlowyOptionTile.text(
|
||||
MobileQuickActionButton(
|
||||
text: LocaleKeys.button_duplicate.tr(),
|
||||
leftIcon: const FlowySvg(
|
||||
FlowySvgs.m_duplicate_s,
|
||||
),
|
||||
showTopBorder: false,
|
||||
icon: FlowySvgs.m_duplicate_s,
|
||||
onTap: () => onAction(
|
||||
MobileViewItemBottomSheetBodyAction.duplicate,
|
||||
),
|
||||
),
|
||||
FlowyOptionTile.text(
|
||||
MobileQuickActionButton(
|
||||
text: LocaleKeys.button_delete.tr(),
|
||||
textColor: Theme.of(context).colorScheme.error,
|
||||
leftIcon: FlowySvg(
|
||||
FlowySvgs.m_delete_s,
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
showTopBorder: false,
|
||||
icon: FlowySvgs.m_delete_s,
|
||||
iconColor: Theme.of(context).colorScheme.error,
|
||||
onTap: () => onAction(
|
||||
MobileViewItemBottomSheetBodyAction.delete,
|
||||
),
|
||||
|
||||
+19
-27
@@ -1,9 +1,10 @@
|
||||
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/mobile/presentation/widgets/widgets.dart';
|
||||
import 'package:appflowy/mobile/presentation/widgets/flowy_mobile_quick_action_button.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
enum MobileViewBottomSheetBodyAction {
|
||||
@@ -84,55 +85,46 @@ class MobileViewBottomSheetBody extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isFavorite = view.isFavorite;
|
||||
return Column(
|
||||
return SeparatedColumn(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
separatorBuilder: () => const Divider(
|
||||
height: 8.5,
|
||||
thickness: 0.5,
|
||||
),
|
||||
children: [
|
||||
FlowyOptionTile.text(
|
||||
MobileQuickActionButton(
|
||||
text: LocaleKeys.button_rename.tr(),
|
||||
leftIcon: const FlowySvg(
|
||||
FlowySvgs.m_rename_s,
|
||||
),
|
||||
showTopBorder: false,
|
||||
icon: FlowySvgs.m_rename_s,
|
||||
onTap: () => onAction(
|
||||
MobileViewBottomSheetBodyAction.rename,
|
||||
),
|
||||
),
|
||||
FlowyOptionTile.text(
|
||||
MobileQuickActionButton(
|
||||
text: isFavorite
|
||||
? LocaleKeys.button_removeFromFavorites.tr()
|
||||
: LocaleKeys.button_addToFavorites.tr(),
|
||||
leftIcon: FlowySvg(
|
||||
size: const Size(20, 20),
|
||||
isFavorite
|
||||
? FlowySvgs.m_favorite_selected_lg
|
||||
: FlowySvgs.m_favorite_unselected_lg,
|
||||
color: isFavorite ? Colors.yellow : null,
|
||||
),
|
||||
showTopBorder: false,
|
||||
icon: isFavorite
|
||||
? FlowySvgs.m_favorite_selected_lg
|
||||
: FlowySvgs.m_favorite_unselected_lg,
|
||||
iconColor: isFavorite ? Colors.yellow : null,
|
||||
onTap: () => onAction(
|
||||
isFavorite
|
||||
? MobileViewBottomSheetBodyAction.removeFromFavorites
|
||||
: MobileViewBottomSheetBodyAction.addToFavorites,
|
||||
),
|
||||
),
|
||||
FlowyOptionTile.text(
|
||||
MobileQuickActionButton(
|
||||
text: LocaleKeys.button_duplicate.tr(),
|
||||
leftIcon: const FlowySvg(
|
||||
FlowySvgs.m_duplicate_s,
|
||||
),
|
||||
showTopBorder: false,
|
||||
icon: FlowySvgs.m_duplicate_s,
|
||||
onTap: () => onAction(
|
||||
MobileViewBottomSheetBodyAction.duplicate,
|
||||
),
|
||||
),
|
||||
FlowyOptionTile.text(
|
||||
MobileQuickActionButton(
|
||||
text: LocaleKeys.button_delete.tr(),
|
||||
textColor: Theme.of(context).colorScheme.error,
|
||||
leftIcon: FlowySvg(
|
||||
FlowySvgs.m_delete_s,
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
showTopBorder: false,
|
||||
icon: FlowySvgs.m_delete_s,
|
||||
iconColor: Theme.of(context).colorScheme.error,
|
||||
onTap: () => onAction(
|
||||
MobileViewBottomSheetBodyAction.delete,
|
||||
),
|
||||
|
||||
+6
@@ -38,6 +38,12 @@ class _MobileBoardContentState extends State<MobileBoardContent> {
|
||||
scrollController = ScrollController();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
scrollController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final screenWidth = MediaQuery.of(context).size.width;
|
||||
|
||||
+6
@@ -19,6 +19,12 @@ class _MobileBoardTrailingState extends State<MobileBoardTrailing> {
|
||||
|
||||
bool isEditing = false;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_textController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final screenSize = MediaQuery.of(context).size;
|
||||
|
||||
+33
-42
@@ -81,6 +81,7 @@ class _MobileRowDetailPageState extends State<MobileRowDetailPage> {
|
||||
child: Scaffold(
|
||||
appBar: FlowyAppBar(
|
||||
leadingType: FlowyAppBarLeadingType.close,
|
||||
showDivider: false,
|
||||
actions: [
|
||||
AppBarMoreButton(
|
||||
onTap: (_) => _showCardActions(context),
|
||||
@@ -135,54 +136,44 @@ class _MobileRowDetailPageState extends State<MobileRowDetailPage> {
|
||||
builder: (_) => Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||
child: MobileQuickActionButton(
|
||||
onTap: () {
|
||||
final rowId = _bloc.state.currentRowId;
|
||||
if (rowId == null) {
|
||||
return;
|
||||
}
|
||||
RowBackendService.duplicateRow(viewId, rowId);
|
||||
context
|
||||
..pop()
|
||||
..pop();
|
||||
Fluttertoast.showToast(
|
||||
msg: LocaleKeys.board_cardDuplicated.tr(),
|
||||
gravity: ToastGravity.BOTTOM,
|
||||
);
|
||||
},
|
||||
icon: FlowySvgs.copy_s,
|
||||
text: LocaleKeys.button_duplicate.tr(),
|
||||
),
|
||||
MobileQuickActionButton(
|
||||
onTap: () =>
|
||||
_performAction(viewId, _bloc.state.currentRowId, false),
|
||||
icon: FlowySvgs.copy_s,
|
||||
text: LocaleKeys.button_duplicate.tr(),
|
||||
),
|
||||
const Divider(height: 9),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||
child: MobileQuickActionButton(
|
||||
onTap: () {
|
||||
final rowId = _bloc.state.currentRowId;
|
||||
if (rowId == null) {
|
||||
return;
|
||||
}
|
||||
RowBackendService.deleteRow(viewId, rowId);
|
||||
context
|
||||
..pop()
|
||||
..pop();
|
||||
Fluttertoast.showToast(
|
||||
msg: LocaleKeys.board_cardDeleted.tr(),
|
||||
gravity: ToastGravity.BOTTOM,
|
||||
);
|
||||
},
|
||||
icon: FlowySvgs.m_delete_m,
|
||||
text: LocaleKeys.button_delete.tr(),
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
const Divider(height: 8.5, thickness: 0.5),
|
||||
MobileQuickActionButton(
|
||||
onTap: () => _performAction(viewId, _bloc.state.currentRowId, true),
|
||||
text: LocaleKeys.button_delete.tr(),
|
||||
textColor: Theme.of(context).colorScheme.error,
|
||||
icon: FlowySvgs.m_delete_m,
|
||||
iconColor: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _performAction(String viewId, String? rowId, bool deleteRow) {
|
||||
if (rowId == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
deleteRow
|
||||
? RowBackendService.deleteRow(viewId, rowId)
|
||||
: RowBackendService.duplicateRow(viewId, rowId);
|
||||
|
||||
context
|
||||
..pop()
|
||||
..pop();
|
||||
Fluttertoast.showToast(
|
||||
msg: deleteRow
|
||||
? LocaleKeys.board_cardDeleted.tr()
|
||||
: LocaleKeys.board_cardDuplicated.tr(),
|
||||
gravity: ToastGravity.BOTTOM,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class RowDetailFab extends StatelessWidget {
|
||||
|
||||
-2
@@ -196,7 +196,6 @@ class _MobileFieldEditorState extends State<MobileFieldEditor> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
values = widget.defaultValues;
|
||||
controller.text = values.name;
|
||||
}
|
||||
@@ -204,7 +203,6 @@ class _MobileFieldEditorState extends State<MobileFieldEditor> {
|
||||
@override
|
||||
void dispose() {
|
||||
controller.dispose();
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
||||
+14
-7
@@ -22,15 +22,18 @@ class MobileDatabaseFieldList extends StatelessWidget {
|
||||
const MobileDatabaseFieldList({
|
||||
super.key,
|
||||
required this.databaseController,
|
||||
required this.canCreate,
|
||||
});
|
||||
|
||||
final DatabaseController databaseController;
|
||||
final bool canCreate;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return _MobileDatabaseFieldListBody(
|
||||
databaseController: databaseController,
|
||||
viewId: context.read<ViewBloc>().state.view.id,
|
||||
canCreate: canCreate,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -39,10 +42,12 @@ class _MobileDatabaseFieldListBody extends StatelessWidget {
|
||||
const _MobileDatabaseFieldListBody({
|
||||
required this.databaseController,
|
||||
required this.viewId,
|
||||
required this.canCreate,
|
||||
});
|
||||
|
||||
final DatabaseController databaseController;
|
||||
final String viewId;
|
||||
final bool canCreate;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -113,13 +118,15 @@ class _MobileDatabaseFieldListBody extends StatelessWidget {
|
||||
.add(DatabasePropertyEvent.moveField(from, to));
|
||||
},
|
||||
header: firstCell,
|
||||
footer: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_divider(),
|
||||
_NewDatabaseFieldTile(viewId: viewId),
|
||||
],
|
||||
),
|
||||
footer: canCreate
|
||||
? Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_divider(),
|
||||
_NewDatabaseFieldTile(viewId: viewId),
|
||||
],
|
||||
)
|
||||
: null,
|
||||
itemCount: cells.length,
|
||||
itemBuilder: (context, index) => cells[index],
|
||||
);
|
||||
|
||||
+7
-9
@@ -72,18 +72,16 @@ class MobileDatabaseViewQuickActions extends StatelessWidget {
|
||||
_Action action,
|
||||
VoidCallback onTap,
|
||||
) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||
child: MobileQuickActionButton(
|
||||
icon: action.icon,
|
||||
text: action.label,
|
||||
color: action.color(context),
|
||||
onTap: onTap,
|
||||
),
|
||||
return MobileQuickActionButton(
|
||||
icon: action.icon,
|
||||
text: action.label,
|
||||
textColor: action.color(context),
|
||||
iconColor: action.color(context),
|
||||
onTap: onTap,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _divider() => const Divider(height: 9);
|
||||
Widget _divider() => const Divider(height: 8.5, thickness: 0.5);
|
||||
}
|
||||
|
||||
enum _Action {
|
||||
|
||||
+7
@@ -83,6 +83,12 @@ class _NameAndIconState extends State<_NameAndIcon> {
|
||||
textEditingController.text = widget.view.name;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
textEditingController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FlowyOptionTile.textField(
|
||||
@@ -240,6 +246,7 @@ class DatabaseViewSettingTile extends StatelessWidget {
|
||||
value: context.read<ViewBloc>(),
|
||||
child: MobileDatabaseFieldList(
|
||||
databaseController: databaseController,
|
||||
canCreate: true,
|
||||
),
|
||||
);
|
||||
},
|
||||
|
||||
@@ -1,99 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
// TODO(yijing): delete this after implementing the real screen inside bottom navigation bar.
|
||||
/// For demonstration purposes
|
||||
class DetailsPlaceholderScreen extends StatefulWidget {
|
||||
/// Constructs a [DetailsScreen].
|
||||
const DetailsPlaceholderScreen({
|
||||
required this.label,
|
||||
this.param,
|
||||
this.extra,
|
||||
this.withScaffold = true,
|
||||
super.key,
|
||||
});
|
||||
|
||||
/// The label to display in the center of the screen.
|
||||
final String label;
|
||||
|
||||
/// Optional param
|
||||
final String? param;
|
||||
|
||||
/// Optional extra object
|
||||
final Object? extra;
|
||||
|
||||
/// Wrap in scaffold
|
||||
final bool withScaffold;
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => DetailsPlaceholderScreenState();
|
||||
}
|
||||
|
||||
/// The state for DetailsScreen
|
||||
class DetailsPlaceholderScreenState extends State<DetailsPlaceholderScreen> {
|
||||
int _counter = 0;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (widget.withScaffold) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text('Details Screen - ${widget.label}'),
|
||||
),
|
||||
body: _build(context),
|
||||
);
|
||||
} else {
|
||||
return Container(
|
||||
color: Theme.of(context).scaffoldBackgroundColor,
|
||||
child: _build(context),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _build(BuildContext context) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
'Details for ${widget.label} - Counter: $_counter',
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
const Padding(padding: EdgeInsets.all(4)),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_counter++;
|
||||
});
|
||||
},
|
||||
child: const Text('Increment counter'),
|
||||
),
|
||||
const Padding(padding: EdgeInsets.all(8)),
|
||||
if (widget.param != null)
|
||||
Text(
|
||||
'Parameter: ${widget.param!}',
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
const Padding(padding: EdgeInsets.all(8)),
|
||||
if (widget.extra != null)
|
||||
Text(
|
||||
'Extra: ${widget.extra!}',
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
if (!widget.withScaffold) ...<Widget>[
|
||||
const Padding(padding: EdgeInsets.all(16)),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
GoRouter.of(context).pop();
|
||||
},
|
||||
child: const Text(
|
||||
'< Back',
|
||||
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -164,23 +164,23 @@ class _DeletedFilesListView extends StatelessWidget {
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: ListView.builder(
|
||||
itemBuilder: (context, index) {
|
||||
final object = state.objects[index];
|
||||
final deletedFile = state.objects[index];
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
|
||||
child: ListTile(
|
||||
// TODO(Yijing): implement file type after TrashPB has file type
|
||||
// TODO: show different file type icon, implement this feature after TrashPB has file type field
|
||||
leading: FlowySvg(
|
||||
FlowySvgs.document_s,
|
||||
size: const Size.square(24),
|
||||
color: theme.colorScheme.onSurface,
|
||||
),
|
||||
title: Text(
|
||||
object.name,
|
||||
deletedFile.name,
|
||||
style: theme.textTheme.labelMedium
|
||||
?.copyWith(color: theme.colorScheme.onBackground),
|
||||
),
|
||||
horizontalTitleGap: 0,
|
||||
// TODO(yiing): needs improve by container/surface theme color
|
||||
tileColor: theme.colorScheme.onSurface.withOpacity(0.1),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
@@ -188,7 +188,6 @@ class _DeletedFilesListView extends StatelessWidget {
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// TODO(yijing): extract icon button
|
||||
IconButton(
|
||||
splashRadius: 20,
|
||||
icon: FlowySvg(
|
||||
@@ -199,10 +198,10 @@ class _DeletedFilesListView extends StatelessWidget {
|
||||
onPressed: () {
|
||||
context
|
||||
.read<TrashBloc>()
|
||||
.add(TrashEvent.putback(object.id));
|
||||
.add(TrashEvent.putback(deletedFile.id));
|
||||
Fluttertoast.showToast(
|
||||
msg:
|
||||
'${object.name} ${LocaleKeys.trash_mobile_isRestored.tr()}',
|
||||
'${deletedFile.name} ${LocaleKeys.trash_mobile_isRestored.tr()}',
|
||||
gravity: ToastGravity.BOTTOM,
|
||||
);
|
||||
},
|
||||
@@ -215,10 +214,12 @@ class _DeletedFilesListView extends StatelessWidget {
|
||||
color: theme.colorScheme.onSurface,
|
||||
),
|
||||
onPressed: () {
|
||||
context.read<TrashBloc>().add(TrashEvent.delete(object));
|
||||
context
|
||||
.read<TrashBloc>()
|
||||
.add(TrashEvent.delete(deletedFile));
|
||||
Fluttertoast.showToast(
|
||||
msg:
|
||||
'${object.name} ${LocaleKeys.trash_mobile_isDeleted.tr()}',
|
||||
'${deletedFile.name} ${LocaleKeys.trash_mobile_isDeleted.tr()}',
|
||||
gravity: ToastGravity.BOTTOM,
|
||||
);
|
||||
},
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
export 'details_placeholder_page.dart';
|
||||
export 'editor/mobile_editor_screen.dart';
|
||||
export 'home/home.dart';
|
||||
export 'mobile_bottom_navigation_bar.dart';
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@ class NotificationsSettingGroup extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _NotificationsSettingGroupState extends State<NotificationsSettingGroup> {
|
||||
// TODO(yijing):remove this after notification page is implemented
|
||||
// TODO:remove this after notification page is implemented
|
||||
bool isPushNotificationOn = false;
|
||||
|
||||
@override
|
||||
|
||||
+1
@@ -23,6 +23,7 @@ class _EditUsernameBottomSheetState extends State<EditUsernameBottomSheet> {
|
||||
late TextEditingController _textFieldController;
|
||||
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
+30
-14
@@ -8,28 +8,44 @@ class MobileQuickActionButton extends StatelessWidget {
|
||||
required this.onTap,
|
||||
required this.icon,
|
||||
required this.text,
|
||||
this.color,
|
||||
this.textColor,
|
||||
this.iconColor,
|
||||
});
|
||||
|
||||
final VoidCallback onTap;
|
||||
final FlowySvgData icon;
|
||||
final String text;
|
||||
final Color? color;
|
||||
final Color? textColor;
|
||||
final Color? iconColor;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Container(
|
||||
height: 44,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
child: Row(
|
||||
children: [
|
||||
FlowySvg(icon, size: const Size.square(20), color: color),
|
||||
const HSpace(8),
|
||||
FlowyText(text, fontSize: 15, color: color),
|
||||
],
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
splashColor: Colors.transparent,
|
||||
child: Container(
|
||||
height: 44,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
child: Row(
|
||||
children: [
|
||||
FlowySvg(
|
||||
icon,
|
||||
size: const Size.square(20),
|
||||
color: iconColor,
|
||||
),
|
||||
const HSpace(12),
|
||||
Expanded(
|
||||
child: FlowyText(
|
||||
text,
|
||||
fontSize: 15,
|
||||
color: textColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
|
||||
extension CalcTypeLabel on CalculationType {
|
||||
String get label => switch (this) {
|
||||
CalculationType.Average =>
|
||||
LocaleKeys.grid_calculationTypeLabel_average.tr(),
|
||||
CalculationType.Max => LocaleKeys.grid_calculationTypeLabel_max.tr(),
|
||||
CalculationType.Median =>
|
||||
LocaleKeys.grid_calculationTypeLabel_median.tr(),
|
||||
CalculationType.Min => LocaleKeys.grid_calculationTypeLabel_min.tr(),
|
||||
CalculationType.Sum => LocaleKeys.grid_calculationTypeLabel_sum.tr(),
|
||||
_ => throw UnimplementedError(
|
||||
'Label for $this has not been implemented',
|
||||
),
|
||||
};
|
||||
}
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
import 'dart:async';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:appflowy/core/notification/grid_notification.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:dartz/dartz.dart';
|
||||
import 'package:flowy_infra/notifier.dart';
|
||||
|
||||
typedef UpdateCalculationValue
|
||||
= Either<CalculationChangesetNotificationPB, FlowyError>;
|
||||
|
||||
class CalculationsListener {
|
||||
CalculationsListener({required this.viewId});
|
||||
|
||||
final String viewId;
|
||||
|
||||
PublishNotifier<UpdateCalculationValue>? _calculationNotifier =
|
||||
PublishNotifier();
|
||||
DatabaseNotificationListener? _listener;
|
||||
|
||||
void start({
|
||||
required void Function(UpdateCalculationValue) onCalculationChanged,
|
||||
}) {
|
||||
_calculationNotifier?.addPublishListener(onCalculationChanged);
|
||||
_listener = DatabaseNotificationListener(
|
||||
objectId: viewId,
|
||||
handler: _handler,
|
||||
);
|
||||
}
|
||||
|
||||
void _handler(
|
||||
DatabaseNotification ty,
|
||||
Either<Uint8List, FlowyError> result,
|
||||
) {
|
||||
switch (ty) {
|
||||
case DatabaseNotification.DidUpdateCalculation:
|
||||
_calculationNotifier?.value = result.fold(
|
||||
(payload) => left(
|
||||
CalculationChangesetNotificationPB.fromBuffer(payload),
|
||||
),
|
||||
(err) => right(err),
|
||||
);
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> stop() async {
|
||||
await _listener?.stop();
|
||||
_calculationNotifier?.dispose();
|
||||
_calculationNotifier = null;
|
||||
}
|
||||
}
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:dartz/dartz.dart';
|
||||
|
||||
class CalculationsBackendService {
|
||||
const CalculationsBackendService({required this.viewId});
|
||||
|
||||
final String viewId;
|
||||
|
||||
// Get Calculations (initial fetch)
|
||||
Future<Either<RepeatedCalculationsPB, FlowyError>> getCalculations() async {
|
||||
final payload = DatabaseViewIdPB()..value = viewId;
|
||||
|
||||
return DatabaseEventGetAllCalculations(payload).send();
|
||||
}
|
||||
|
||||
Future<void> updateCalculation(
|
||||
String fieldId,
|
||||
CalculationType type, {
|
||||
String? calculationId,
|
||||
}) async {
|
||||
final payload = UpdateCalculationChangesetPB()
|
||||
..viewId = viewId
|
||||
..fieldId = fieldId
|
||||
..calculationType = type;
|
||||
|
||||
if (calculationId != null) {
|
||||
payload.calculationId = calculationId;
|
||||
}
|
||||
|
||||
await DatabaseEventUpdateCalculation(payload).send();
|
||||
}
|
||||
|
||||
Future<void> removeCalculation(
|
||||
String fieldId,
|
||||
String calculationId,
|
||||
) async {
|
||||
final payload = RemoveCalculationChangesetPB()
|
||||
..viewId = viewId
|
||||
..fieldId = fieldId
|
||||
..calculationId = calculationId;
|
||||
|
||||
await DatabaseEventRemoveCalculation(payload).send();
|
||||
}
|
||||
}
|
||||
+10
-11
@@ -16,6 +16,16 @@ class CheckboxCellBloc extends Bloc<CheckboxCellEvent, CheckboxCellState> {
|
||||
final CheckboxCellController cellController;
|
||||
void Function()? _onCellChangedFn;
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
if (_onCellChangedFn != null) {
|
||||
cellController.removeListener(_onCellChangedFn!);
|
||||
_onCellChangedFn = null;
|
||||
}
|
||||
await cellController.dispose();
|
||||
return super.close();
|
||||
}
|
||||
|
||||
void _dispatch() {
|
||||
on<CheckboxCellEvent>(
|
||||
(event, emit) {
|
||||
@@ -35,17 +45,6 @@ class CheckboxCellBloc extends Bloc<CheckboxCellEvent, CheckboxCellState> {
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
if (_onCellChangedFn != null) {
|
||||
cellController.removeListener(_onCellChangedFn!);
|
||||
_onCellChangedFn = null;
|
||||
}
|
||||
|
||||
await cellController.dispose();
|
||||
return super.close();
|
||||
}
|
||||
|
||||
void _startListening() {
|
||||
_onCellChangedFn = cellController.addListener(
|
||||
onCellChanged: (cellData) {
|
||||
|
||||
+10
-10
@@ -32,6 +32,16 @@ class ChecklistCellBloc extends Bloc<ChecklistCellEvent, ChecklistCellState> {
|
||||
final ChecklistCellBackendService _checklistCellService;
|
||||
void Function()? _onCellChangedFn;
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
if (_onCellChangedFn != null) {
|
||||
cellController.removeListener(_onCellChangedFn!);
|
||||
_onCellChangedFn = null;
|
||||
}
|
||||
await cellController.dispose();
|
||||
return super.close();
|
||||
}
|
||||
|
||||
void _dispatch() {
|
||||
on<ChecklistCellEvent>(
|
||||
(event, emit) async {
|
||||
@@ -79,16 +89,6 @@ class ChecklistCellBloc extends Bloc<ChecklistCellEvent, ChecklistCellState> {
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
if (_onCellChangedFn != null) {
|
||||
cellController.removeListener(_onCellChangedFn!);
|
||||
_onCellChangedFn = null;
|
||||
}
|
||||
await cellController.dispose();
|
||||
return super.close();
|
||||
}
|
||||
|
||||
void _startListening() {
|
||||
_onCellChangedFn = cellController.addListener(
|
||||
onCellChanged: (data) {
|
||||
|
||||
+10
-9
@@ -17,6 +17,16 @@ class DateCellBloc extends Bloc<DateCellEvent, DateCellState> {
|
||||
final DateCellController cellController;
|
||||
void Function()? _onCellChangedFn;
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
if (_onCellChangedFn != null) {
|
||||
cellController.removeListener(_onCellChangedFn!);
|
||||
_onCellChangedFn = null;
|
||||
}
|
||||
await cellController.dispose();
|
||||
return super.close();
|
||||
}
|
||||
|
||||
void _dispatch() {
|
||||
on<DateCellEvent>(
|
||||
(event, emit) async {
|
||||
@@ -35,15 +45,6 @@ class DateCellBloc extends Bloc<DateCellEvent, DateCellState> {
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
if (_onCellChangedFn != null) {
|
||||
cellController.removeListener(_onCellChangedFn!);
|
||||
_onCellChangedFn = null;
|
||||
}
|
||||
return super.close();
|
||||
}
|
||||
|
||||
void _startListening() {
|
||||
_onCellChangedFn = cellController.addListener(
|
||||
onCellChanged: (data) {
|
||||
|
||||
+10
-10
@@ -15,6 +15,16 @@ class NumberCellBloc extends Bloc<NumberCellEvent, NumberCellState> {
|
||||
final NumberCellController cellController;
|
||||
void Function()? _onCellChangedFn;
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
if (_onCellChangedFn != null) {
|
||||
cellController.removeListener(_onCellChangedFn!);
|
||||
_onCellChangedFn = null;
|
||||
}
|
||||
await cellController.dispose();
|
||||
return super.close();
|
||||
}
|
||||
|
||||
void _dispatch() {
|
||||
on<NumberCellEvent>(
|
||||
(event, emit) async {
|
||||
@@ -45,16 +55,6 @@ class NumberCellBloc extends Bloc<NumberCellEvent, NumberCellState> {
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
if (_onCellChangedFn != null) {
|
||||
cellController.removeListener(_onCellChangedFn!);
|
||||
_onCellChangedFn = null;
|
||||
}
|
||||
await cellController.dispose();
|
||||
return super.close();
|
||||
}
|
||||
|
||||
void _startListening() {
|
||||
_onCellChangedFn = cellController.addListener(
|
||||
onCellChanged: (cellContent) {
|
||||
|
||||
+10
-10
@@ -17,6 +17,16 @@ class SelectOptionCellBloc
|
||||
final SelectOptionCellController cellController;
|
||||
void Function()? _onCellChangedFn;
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
if (_onCellChangedFn != null) {
|
||||
cellController.removeListener(_onCellChangedFn!);
|
||||
_onCellChangedFn = null;
|
||||
}
|
||||
await cellController.dispose();
|
||||
return super.close();
|
||||
}
|
||||
|
||||
void _dispatch() {
|
||||
on<SelectOptionCellEvent>(
|
||||
(event, emit) async {
|
||||
@@ -36,16 +46,6 @@ class SelectOptionCellBloc
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
if (_onCellChangedFn != null) {
|
||||
cellController.removeListener(_onCellChangedFn!);
|
||||
_onCellChangedFn = null;
|
||||
}
|
||||
await cellController.dispose();
|
||||
return super.close();
|
||||
}
|
||||
|
||||
void _startListening() {
|
||||
_onCellChangedFn = cellController.addListener(
|
||||
onCellChanged: (selectOptionCellData) {
|
||||
|
||||
+10
-10
@@ -15,6 +15,16 @@ class TextCellBloc extends Bloc<TextCellEvent, TextCellState> {
|
||||
final TextCellController cellController;
|
||||
void Function()? _onCellChangedFn;
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
if (_onCellChangedFn != null) {
|
||||
cellController.removeListener(_onCellChangedFn!);
|
||||
_onCellChangedFn = null;
|
||||
}
|
||||
await cellController.dispose();
|
||||
return super.close();
|
||||
}
|
||||
|
||||
void _dispatch() {
|
||||
on<TextCellEvent>(
|
||||
(event, emit) {
|
||||
@@ -41,16 +51,6 @@ class TextCellBloc extends Bloc<TextCellEvent, TextCellState> {
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
if (_onCellChangedFn != null) {
|
||||
cellController.removeListener(_onCellChangedFn!);
|
||||
_onCellChangedFn = null;
|
||||
}
|
||||
await cellController.dispose();
|
||||
return super.close();
|
||||
}
|
||||
|
||||
void _startListening() {
|
||||
_onCellChangedFn = cellController.addListener(
|
||||
onCellChanged: (cellContent) {
|
||||
|
||||
+10
-10
@@ -17,6 +17,16 @@ class TimestampCellBloc extends Bloc<TimestampCellEvent, TimestampCellState> {
|
||||
final TimestampCellController cellController;
|
||||
void Function()? _onCellChangedFn;
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
if (_onCellChangedFn != null) {
|
||||
cellController.removeListener(_onCellChangedFn!);
|
||||
_onCellChangedFn = null;
|
||||
}
|
||||
await cellController.dispose();
|
||||
return super.close();
|
||||
}
|
||||
|
||||
void _dispatch() {
|
||||
on<TimestampCellEvent>(
|
||||
(event, emit) async {
|
||||
@@ -35,16 +45,6 @@ class TimestampCellBloc extends Bloc<TimestampCellEvent, TimestampCellState> {
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
if (_onCellChangedFn != null) {
|
||||
cellController.removeListener(_onCellChangedFn!);
|
||||
_onCellChangedFn = null;
|
||||
}
|
||||
await cellController.dispose();
|
||||
return super.close();
|
||||
}
|
||||
|
||||
void _startListening() {
|
||||
_onCellChangedFn = cellController.addListener(
|
||||
onCellChanged: (data) {
|
||||
|
||||
+10
-10
@@ -16,6 +16,16 @@ class URLCellBloc extends Bloc<URLCellEvent, URLCellState> {
|
||||
final URLCellController cellController;
|
||||
void Function()? _onCellChangedFn;
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
if (_onCellChangedFn != null) {
|
||||
cellController.removeListener(_onCellChangedFn!);
|
||||
_onCellChangedFn = null;
|
||||
}
|
||||
await cellController.dispose();
|
||||
return super.close();
|
||||
}
|
||||
|
||||
void _dispatch() {
|
||||
on<URLCellEvent>(
|
||||
(event, emit) async {
|
||||
@@ -39,16 +49,6 @@ class URLCellBloc extends Bloc<URLCellEvent, URLCellState> {
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
if (_onCellChangedFn != null) {
|
||||
cellController.removeListener(_onCellChangedFn!);
|
||||
_onCellChangedFn = null;
|
||||
}
|
||||
await cellController.dispose();
|
||||
return super.close();
|
||||
}
|
||||
|
||||
void _startListening() {
|
||||
_onCellChangedFn = cellController.addListener(
|
||||
onCellChanged: (cellData) {
|
||||
|
||||
+7
-4
@@ -282,16 +282,19 @@ class FieldController {
|
||||
) {
|
||||
for (final newSortPB in changeset.insertSorts) {
|
||||
final sortIndex = newSortInfos
|
||||
.indexWhere((element) => element.sortId == newSortPB.id);
|
||||
.indexWhere((element) => element.sortId == newSortPB.sort.id);
|
||||
if (sortIndex == -1) {
|
||||
final fieldInfo = _findFieldInfo(
|
||||
fieldInfos: fieldInfos,
|
||||
fieldId: newSortPB.fieldId,
|
||||
fieldType: newSortPB.fieldType,
|
||||
fieldId: newSortPB.sort.fieldId,
|
||||
fieldType: newSortPB.sort.fieldType,
|
||||
);
|
||||
|
||||
if (fieldInfo != null) {
|
||||
newSortInfos.add(SortInfo(sortPB: newSortPB, fieldInfo: fieldInfo));
|
||||
newSortInfos.insert(
|
||||
newSortPB.index,
|
||||
SortInfo(sortPB: newSortPB.sort, fieldInfo: fieldInfo),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,6 +71,8 @@ class FieldInfo with _$FieldInfo {
|
||||
case FieldType.DateTime:
|
||||
case FieldType.SingleSelect:
|
||||
case FieldType.MultiSelect:
|
||||
case FieldType.LastEditedTime:
|
||||
case FieldType.CreatedTime:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
|
||||
-52
@@ -1,52 +0,0 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:appflowy/core/notification/grid_notification.dart';
|
||||
import 'package:flowy_infra/notifier.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/protobuf.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||
import 'package:dartz/dartz.dart';
|
||||
|
||||
/// Listener for database layout changes.
|
||||
class DatabaseLayoutListener {
|
||||
DatabaseLayoutListener(this.viewId);
|
||||
|
||||
final String viewId;
|
||||
|
||||
PublishNotifier<Either<DatabaseLayoutPB, FlowyError>>? _layoutNotifier =
|
||||
PublishNotifier();
|
||||
DatabaseNotificationListener? _listener;
|
||||
|
||||
void start({
|
||||
required void Function(Either<DatabaseLayoutPB, FlowyError>)
|
||||
onLayoutChanged,
|
||||
}) {
|
||||
_layoutNotifier?.addPublishListener(onLayoutChanged);
|
||||
_listener = DatabaseNotificationListener(
|
||||
objectId: viewId,
|
||||
handler: _handler,
|
||||
);
|
||||
}
|
||||
|
||||
void _handler(
|
||||
DatabaseNotification ty,
|
||||
Either<Uint8List, FlowyError> result,
|
||||
) {
|
||||
switch (ty) {
|
||||
case DatabaseNotification.DidUpdateDatabaseLayout:
|
||||
result.fold(
|
||||
(payload) => _layoutNotifier?.value =
|
||||
left(DatabaseLayoutMetaPB.fromBuffer(payload).layout),
|
||||
(error) => _layoutNotifier?.value = right(error),
|
||||
);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> stop() async {
|
||||
await _listener?.stop();
|
||||
_layoutNotifier?.dispose();
|
||||
_layoutNotifier = null;
|
||||
}
|
||||
}
|
||||
-62
@@ -1,62 +0,0 @@
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pb.dart';
|
||||
import 'setting_listener.dart';
|
||||
import 'setting_service.dart';
|
||||
|
||||
typedef OnError = void Function(FlowyError);
|
||||
typedef OnSettingUpdated = void Function(DatabaseViewSettingPB);
|
||||
|
||||
class SettingController {
|
||||
SettingController({
|
||||
required this.viewId,
|
||||
}) : _settingBackendSvc = SettingBackendService(viewId: viewId),
|
||||
_listener = DatabaseSettingListener(viewId: viewId) {
|
||||
// Load setting
|
||||
_settingBackendSvc.getSetting().then((result) {
|
||||
result.fold(
|
||||
(newSetting) => updateSetting(newSetting),
|
||||
(err) => _onError?.call(err),
|
||||
);
|
||||
});
|
||||
|
||||
// Listen on the setting changes
|
||||
_listener.start(
|
||||
onSettingUpdated: (result) {
|
||||
result.fold(
|
||||
(newSetting) => updateSetting(newSetting),
|
||||
(err) => _onError?.call(err),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
final String viewId;
|
||||
final SettingBackendService _settingBackendSvc;
|
||||
final DatabaseSettingListener _listener;
|
||||
|
||||
OnSettingUpdated? _onSettingUpdated;
|
||||
OnError? _onError;
|
||||
DatabaseViewSettingPB? _setting;
|
||||
DatabaseViewSettingPB? get setting => _setting;
|
||||
|
||||
void startListening({
|
||||
required OnSettingUpdated onSettingUpdated,
|
||||
required OnError onError,
|
||||
}) {
|
||||
assert(_onSettingUpdated == null, 'Should call once');
|
||||
assert(_onError == null, 'Should call once');
|
||||
_onSettingUpdated = onSettingUpdated;
|
||||
_onError = onError;
|
||||
}
|
||||
|
||||
void updateSetting(DatabaseViewSettingPB newSetting) {
|
||||
_setting = newSetting;
|
||||
_onSettingUpdated?.call(newSetting);
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
_onSettingUpdated = null;
|
||||
_onError = null;
|
||||
_listener.stop();
|
||||
}
|
||||
}
|
||||
@@ -75,16 +75,28 @@ class SortBackendService {
|
||||
});
|
||||
}
|
||||
|
||||
Future<Either<Unit, FlowyError>> reorderSort({
|
||||
required String fromSortId,
|
||||
required String toSortId,
|
||||
}) {
|
||||
final payload = DatabaseSettingChangesetPB()
|
||||
..viewId = viewId
|
||||
..reorderSort = (ReorderSortPayloadPB()
|
||||
..viewId = viewId
|
||||
..fromSortId = fromSortId
|
||||
..toSortId = toSortId);
|
||||
|
||||
return DatabaseEventUpdateDatabaseSetting(payload).send();
|
||||
}
|
||||
|
||||
Future<Either<Unit, FlowyError>> deleteSort({
|
||||
required String fieldId,
|
||||
required String sortId,
|
||||
required FieldType fieldType,
|
||||
}) {
|
||||
final deleteSortPayload = DeleteSortPayloadPB.create()
|
||||
..fieldId = fieldId
|
||||
..sortId = sortId
|
||||
..viewId = viewId
|
||||
..fieldType = fieldType;
|
||||
..viewId = viewId;
|
||||
|
||||
final payload = DatabaseSettingChangesetPB.create()
|
||||
..viewId = viewId
|
||||
|
||||
@@ -131,6 +131,7 @@ class DatabaseTabBarBloc
|
||||
Future<void> close() async {
|
||||
for (final tabBar in state.tabBars) {
|
||||
await state.tabBarControllerByViewId[tabBar.viewId]?.dispose();
|
||||
tabBar.dispose();
|
||||
}
|
||||
return super.close();
|
||||
}
|
||||
@@ -261,6 +262,10 @@ class DatabaseTabBar extends Equatable {
|
||||
|
||||
@override
|
||||
List<Object?> get props => [view.hashCode];
|
||||
|
||||
void dispose() {
|
||||
_builder.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
typedef OnViewUpdated = void Function(ViewPB newView);
|
||||
|
||||
@@ -33,7 +33,7 @@ import '../application/board_bloc.dart';
|
||||
import 'toolbar/board_setting_bar.dart';
|
||||
import 'widgets/board_hidden_groups.dart';
|
||||
|
||||
class BoardPageTabBarBuilderImpl implements DatabaseTabBarItemBuilder {
|
||||
class BoardPageTabBarBuilderImpl extends DatabaseTabBarItemBuilder {
|
||||
@override
|
||||
Widget content(
|
||||
BuildContext context,
|
||||
|
||||
+6
-6
@@ -26,6 +26,12 @@ class CalendarSettingBloc
|
||||
final DatabaseController _databaseController;
|
||||
final DatabaseLayoutSettingListener _listener;
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
await _listener.stop();
|
||||
return super.close();
|
||||
}
|
||||
|
||||
void _dispatch() {
|
||||
on<CalendarSettingEvent>((event, emit) {
|
||||
event.when(
|
||||
@@ -108,12 +114,6 @@ class CalendarSettingBloc
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
await _listener.stop();
|
||||
return super.close();
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
|
||||
+1
-1
@@ -30,7 +30,7 @@ import 'calendar_day.dart';
|
||||
import 'layout/sizes.dart';
|
||||
import 'toolbar/calendar_setting_bar.dart';
|
||||
|
||||
class CalendarPageTabBarBuilderImpl implements DatabaseTabBarItemBuilder {
|
||||
class CalendarPageTabBarBuilderImpl extends DatabaseTabBarItemBuilder {
|
||||
@override
|
||||
Widget content(
|
||||
BuildContext context,
|
||||
|
||||
+183
@@ -0,0 +1,183 @@
|
||||
import 'package:appflowy/plugins/database/application/calculations/calculations_listener.dart';
|
||||
import 'package:appflowy/plugins/database/application/calculations/calculations_service.dart';
|
||||
import 'package:appflowy/plugins/database/application/field/field_controller.dart';
|
||||
import 'package:appflowy/plugins/database/application/field/field_info.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_service.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/calculation_entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/field_settings_entities.pbenum.dart';
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'calculations_bloc.freezed.dart';
|
||||
|
||||
class CalculationsBloc extends Bloc<CalculationsEvent, CalculationsState> {
|
||||
CalculationsBloc({
|
||||
required this.viewId,
|
||||
required FieldController fieldController,
|
||||
}) : _fieldController = fieldController,
|
||||
_calculationsListener = CalculationsListener(viewId: viewId),
|
||||
_calculationsService = CalculationsBackendService(viewId: viewId),
|
||||
super(CalculationsState.initial()) {
|
||||
_dispatch();
|
||||
}
|
||||
|
||||
final String viewId;
|
||||
final FieldController _fieldController;
|
||||
final CalculationsListener _calculationsListener;
|
||||
late final CalculationsBackendService _calculationsService;
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
_fieldController.removeListener(onFieldsListener: _onReceiveFields);
|
||||
await _calculationsListener.stop();
|
||||
await super.close();
|
||||
}
|
||||
|
||||
void _dispatch() {
|
||||
on<CalculationsEvent>((event, emit) async {
|
||||
await event.when(
|
||||
started: () async {
|
||||
_startListening();
|
||||
await _getAllCalculations();
|
||||
|
||||
add(
|
||||
CalculationsEvent.didReceiveFieldUpdate(
|
||||
_fieldController.fieldInfos,
|
||||
),
|
||||
);
|
||||
},
|
||||
didReceiveFieldUpdate: (fields) async {
|
||||
emit(
|
||||
state.copyWith(
|
||||
fields: fields
|
||||
.where(
|
||||
(e) =>
|
||||
e.visibility != null &&
|
||||
e.visibility != FieldVisibility.AlwaysHidden,
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
);
|
||||
},
|
||||
didReceiveCalculationsUpdate: (calculationsMap) async {
|
||||
emit(
|
||||
state.copyWith(
|
||||
calculationsByFieldId: calculationsMap,
|
||||
),
|
||||
);
|
||||
},
|
||||
updateCalculationType: (fieldId, type, calculationId) async {
|
||||
await _calculationsService.updateCalculation(
|
||||
fieldId,
|
||||
type,
|
||||
calculationId: calculationId,
|
||||
);
|
||||
},
|
||||
removeCalculation: (fieldId, calculationId) async {
|
||||
await _calculationsService.removeCalculation(fieldId, calculationId);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
void _startListening() {
|
||||
_fieldController.addListener(
|
||||
listenWhen: () => !isClosed,
|
||||
onReceiveFields: _onReceiveFields,
|
||||
);
|
||||
|
||||
_calculationsListener.start(
|
||||
onCalculationChanged: (changesetOrFailure) {
|
||||
if (isClosed) {
|
||||
return;
|
||||
}
|
||||
|
||||
changesetOrFailure.fold(
|
||||
(changeset) {
|
||||
final calculationsMap = {...state.calculationsByFieldId};
|
||||
if (changeset.insertCalculations.isNotEmpty) {
|
||||
for (final insert in changeset.insertCalculations) {
|
||||
calculationsMap[insert.fieldId] = insert;
|
||||
}
|
||||
}
|
||||
|
||||
if (changeset.updateCalculations.isNotEmpty) {
|
||||
for (final update in changeset.updateCalculations) {
|
||||
calculationsMap.removeWhere((key, _) => key == update.fieldId);
|
||||
calculationsMap.addAll({update.fieldId: update});
|
||||
}
|
||||
}
|
||||
|
||||
if (changeset.deleteCalculations.isNotEmpty) {
|
||||
for (final delete in changeset.deleteCalculations) {
|
||||
calculationsMap.removeWhere((key, _) => key == delete.fieldId);
|
||||
}
|
||||
}
|
||||
|
||||
add(
|
||||
CalculationsEvent.didReceiveCalculationsUpdate(
|
||||
calculationsMap,
|
||||
),
|
||||
);
|
||||
},
|
||||
(_) => null,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _onReceiveFields(List<FieldInfo> fields) =>
|
||||
add(CalculationsEvent.didReceiveFieldUpdate(fields));
|
||||
|
||||
Future<void> _getAllCalculations() async {
|
||||
final calculationsOrFailure = await _calculationsService.getCalculations();
|
||||
|
||||
final RepeatedCalculationsPB? calculations =
|
||||
calculationsOrFailure.getLeftOrNull();
|
||||
if (calculations != null) {
|
||||
final calculationMap = <String, CalculationPB>{};
|
||||
for (final calculation in calculations.items) {
|
||||
calculationMap[calculation.fieldId] = calculation;
|
||||
}
|
||||
|
||||
add(CalculationsEvent.didReceiveCalculationsUpdate(calculationMap));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
class CalculationsEvent with _$CalculationsEvent {
|
||||
const factory CalculationsEvent.started() = _Started;
|
||||
|
||||
const factory CalculationsEvent.didReceiveFieldUpdate(
|
||||
List<FieldInfo> fields,
|
||||
) = _DidReceiveFieldUpdate;
|
||||
|
||||
const factory CalculationsEvent.didReceiveCalculationsUpdate(
|
||||
Map<String, CalculationPB> calculationsByFieldId,
|
||||
) = _DidReceiveCalculationsUpdate;
|
||||
|
||||
const factory CalculationsEvent.updateCalculationType(
|
||||
String fieldId,
|
||||
CalculationType type, {
|
||||
@Default(null) String? calculationId,
|
||||
}) = _UpdateCalculationType;
|
||||
|
||||
const factory CalculationsEvent.removeCalculation(
|
||||
String fieldId,
|
||||
String calculationId,
|
||||
) = _RemoveCalculation;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class CalculationsState with _$CalculationsState {
|
||||
const factory CalculationsState({
|
||||
required List<FieldInfo> fields,
|
||||
required Map<String, CalculationPB> calculationsByFieldId,
|
||||
}) = _CalculationsState;
|
||||
|
||||
factory CalculationsState.initial() => const CalculationsState(
|
||||
fields: [],
|
||||
calculationsByFieldId: {},
|
||||
);
|
||||
}
|
||||
+1
-1
@@ -73,7 +73,7 @@ class GridFilterMenuBloc
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() {
|
||||
Future<void> close() async {
|
||||
if (_onFilterFn != null) {
|
||||
fieldController.removeListener(onFiltersListener: _onFilterFn!);
|
||||
_onFilterFn = null;
|
||||
|
||||
+19
@@ -71,6 +71,23 @@ class SortEditorBloc extends Bloc<SortEditorEvent, SortEditorState> {
|
||||
);
|
||||
result.fold((l) => null, (err) => Log.error(err));
|
||||
},
|
||||
reorderSort: (fromIndex, toIndex) async {
|
||||
if (fromIndex < toIndex) {
|
||||
toIndex--;
|
||||
}
|
||||
|
||||
final fromId = state.sortInfos[fromIndex].sortId;
|
||||
final toId = state.sortInfos[toIndex].sortId;
|
||||
|
||||
final newSorts = [...state.sortInfos];
|
||||
newSorts.insert(toIndex, newSorts.removeAt(fromIndex));
|
||||
emit(state.copyWith(sortInfos: newSorts));
|
||||
final result = await _sortBackendSvc.reorderSort(
|
||||
fromSortId: fromId,
|
||||
toSortId: toId,
|
||||
);
|
||||
result.fold((l) => null, (err) => Log.error(err));
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -113,6 +130,8 @@ class SortEditorEvent with _$SortEditorEvent {
|
||||
) = _SetCondition;
|
||||
const factory SortEditorEvent.deleteSort(SortInfo sortInfo) = _DeleteSort;
|
||||
const factory SortEditorEvent.deleteAllSorts() = _DeleteAllSorts;
|
||||
const factory SortEditorEvent.reorderSort(int oldIndex, int newIndex) =
|
||||
_ReorderSort;
|
||||
}
|
||||
|
||||
@freezed
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/database/application/row/row_service.dart';
|
||||
import 'package:appflowy/plugins/database/grid/presentation/widgets/calculations/calculations_row.dart';
|
||||
import 'package:appflowy/plugins/database/grid/presentation/widgets/toolbar/grid_setting_bar.dart';
|
||||
import 'package:appflowy/plugins/database/tab_bar/desktop/setting_menu.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_builder.dart';
|
||||
@@ -12,8 +13,7 @@ import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/theme_extension.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui_web.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/scrolling/styled_scroll_bar.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/scrolling/styled_scrollview.dart';
|
||||
import 'package:flowy_infra_ui/widget/error_page.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
@@ -45,7 +45,7 @@ class ToggleExtensionNotifier extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
class DesktopGridTabBarBuilderImpl implements DatabaseTabBarItemBuilder {
|
||||
class DesktopGridTabBarBuilderImpl extends DatabaseTabBarItemBuilder {
|
||||
final _toggleExtension = ToggleExtensionNotifier();
|
||||
|
||||
@override
|
||||
@@ -86,6 +86,12 @@ class DesktopGridTabBarBuilderImpl implements DatabaseTabBarItemBuilder {
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_toggleExtension.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
ValueKey _makeValueKey(DatabaseController controller) {
|
||||
return ValueKey(controller.viewId);
|
||||
}
|
||||
@@ -303,17 +309,22 @@ class _GridRows extends StatelessWidget {
|
||||
GridState state,
|
||||
List<RowInfo> rowInfos,
|
||||
) {
|
||||
final children = [
|
||||
...rowInfos.mapIndexed((index, rowInfo) {
|
||||
return _renderRow(
|
||||
context,
|
||||
rowInfo.rowId,
|
||||
isDraggable: state.reorderable,
|
||||
index: index,
|
||||
);
|
||||
}),
|
||||
const GridRowBottomBar(key: Key('gridFooter')),
|
||||
];
|
||||
final children = rowInfos.mapIndexed((index, rowInfo) {
|
||||
return _renderRow(
|
||||
context,
|
||||
rowInfo.rowId,
|
||||
isDraggable: state.reorderable,
|
||||
index: index,
|
||||
);
|
||||
}).toList()
|
||||
..add(const GridRowBottomBar(key: Key('grid_footer')))
|
||||
..add(
|
||||
GridCalculationsRow(
|
||||
key: const Key('grid_calculations'),
|
||||
viewId: viewId,
|
||||
),
|
||||
);
|
||||
|
||||
return ReorderableListView.builder(
|
||||
/// This is a workaround related to
|
||||
/// https://github.com/flutter/flutter/issues/25652
|
||||
@@ -434,9 +445,10 @@ class _GridFooter extends StatelessWidget {
|
||||
child: RichText(
|
||||
text: TextSpan(
|
||||
text: rowCountString(),
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
color: Theme.of(context).hintColor,
|
||||
),
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium!
|
||||
.copyWith(color: Theme.of(context).hintColor),
|
||||
children: [
|
||||
TextSpan(
|
||||
text: ' $rowCount',
|
||||
|
||||
+1
-1
@@ -29,7 +29,7 @@ import 'widgets/header/mobile_grid_header.dart';
|
||||
import 'widgets/mobile_fab.dart';
|
||||
import 'widgets/row/mobile_row.dart';
|
||||
|
||||
class MobileGridTabBarBuilderImpl implements DatabaseTabBarItemBuilder {
|
||||
class MobileGridTabBarBuilderImpl extends DatabaseTabBarItemBuilder {
|
||||
@override
|
||||
Widget content(
|
||||
BuildContext context,
|
||||
|
||||
+137
@@ -0,0 +1,137 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/plugins/database/application/calculations/calculation_type_ext.dart';
|
||||
import 'package:appflowy/plugins/database/application/field/field_info.dart';
|
||||
import 'package:appflowy/plugins/database/grid/application/calculations/calculations_bloc.dart';
|
||||
import 'package:appflowy/plugins/database/grid/presentation/widgets/calculations/calculation_selector.dart';
|
||||
import 'package:appflowy/plugins/database/grid/presentation/widgets/calculations/calculation_type_item.dart';
|
||||
import 'package:appflowy/plugins/database/grid/presentation/widgets/calculations/remove_calculation_button.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/calculation_entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:flowy_infra/theme_extension.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class CalculateCell extends StatefulWidget {
|
||||
const CalculateCell({
|
||||
super.key,
|
||||
required this.fieldInfo,
|
||||
required this.width,
|
||||
this.calculation,
|
||||
});
|
||||
|
||||
final FieldInfo fieldInfo;
|
||||
final double width;
|
||||
final CalculationPB? calculation;
|
||||
|
||||
@override
|
||||
State<CalculateCell> createState() => _CalculateCellState();
|
||||
}
|
||||
|
||||
class _CalculateCellState extends State<CalculateCell> {
|
||||
bool isSelected = false;
|
||||
|
||||
void setIsSelected(bool selected) => setState(() => isSelected = selected);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
height: 35,
|
||||
width: widget.width,
|
||||
child: AppFlowyPopover(
|
||||
constraints: BoxConstraints.loose(const Size(150, 200)),
|
||||
direction: PopoverDirection.bottomWithCenterAligned,
|
||||
onClose: () => setIsSelected(false),
|
||||
popupBuilder: (_) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (mounted) {
|
||||
setIsSelected(true);
|
||||
}
|
||||
});
|
||||
|
||||
return SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
if (widget.calculation != null)
|
||||
RemoveCalculationButton(
|
||||
onTap: () => context.read<CalculationsBloc>().add(
|
||||
CalculationsEvent.removeCalculation(
|
||||
widget.fieldInfo.id,
|
||||
widget.calculation!.id,
|
||||
),
|
||||
),
|
||||
),
|
||||
...CalculationType.values.map(
|
||||
(type) => CalculationTypeItem(
|
||||
type: type,
|
||||
onTap: () {
|
||||
if (type != widget.calculation?.calculationType) {
|
||||
context.read<CalculationsBloc>().add(
|
||||
CalculationsEvent.updateCalculationType(
|
||||
widget.fieldInfo.id,
|
||||
type,
|
||||
calculationId: widget.calculation?.id,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
child: widget.fieldInfo.fieldType == FieldType.Number
|
||||
? widget.calculation != null
|
||||
? _showCalculateValue(context)
|
||||
: CalculationSelector(isSelected: isSelected)
|
||||
: const SizedBox.shrink(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _showCalculateValue(BuildContext context) {
|
||||
return FlowyButton(
|
||||
radius: BorderRadius.zero,
|
||||
hoverColor: AFThemeExtension.of(context).lightGreyHover,
|
||||
text: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
Flexible(
|
||||
child: FlowyText(
|
||||
widget.calculation!.calculationType.label,
|
||||
color: Theme.of(context).hintColor,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
if (widget.calculation!.value.isNotEmpty) ...[
|
||||
const HSpace(8),
|
||||
Flexible(
|
||||
child: FlowyText(
|
||||
_withoutTrailingZeros(widget.calculation!.value),
|
||||
color: AFThemeExtension.of(context).textColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
const HSpace(8),
|
||||
FlowySvg(
|
||||
FlowySvgs.arrow_down_s,
|
||||
color: Theme.of(context).hintColor,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _withoutTrailingZeros(String value) {
|
||||
final regex = RegExp(r'^(\d+(?:\.\d*?[1-9](?=0|\b))?)\.?0*$');
|
||||
if (regex.hasMatch(value)) {
|
||||
final match = regex.firstMatch(value)!;
|
||||
return match.group(1)!;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
}
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/theme_extension.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/button.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||
import 'package:flowy_infra_ui/widget/spacing.dart';
|
||||
|
||||
class CalculationSelector extends StatefulWidget {
|
||||
const CalculationSelector({
|
||||
super.key,
|
||||
required this.isSelected,
|
||||
});
|
||||
|
||||
final bool isSelected;
|
||||
|
||||
@override
|
||||
State<CalculationSelector> createState() => _CalculationSelectorState();
|
||||
}
|
||||
|
||||
class _CalculationSelectorState extends State<CalculationSelector> {
|
||||
bool _isHovering = false;
|
||||
|
||||
void _setHovering(bool isHovering) =>
|
||||
setState(() => _isHovering = isHovering);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MouseRegion(
|
||||
onEnter: (_) => _setHovering(true),
|
||||
onExit: (_) => _setHovering(false),
|
||||
child: AnimatedOpacity(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
opacity: widget.isSelected || _isHovering ? 1 : 0,
|
||||
child: FlowyButton(
|
||||
radius: BorderRadius.zero,
|
||||
hoverColor: AFThemeExtension.of(context).lightGreyHover,
|
||||
text: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
Flexible(
|
||||
child: FlowyText(
|
||||
LocaleKeys.grid_calculate.tr(),
|
||||
color: Theme.of(context).hintColor,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
const HSpace(8),
|
||||
FlowySvg(
|
||||
FlowySvgs.arrow_down_s,
|
||||
color: Theme.of(context).hintColor,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/plugins/database/application/calculations/calculation_type_ext.dart';
|
||||
import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/calculation_entities.pbenum.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/button.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||
|
||||
class CalculationTypeItem extends StatelessWidget {
|
||||
const CalculationTypeItem({
|
||||
super.key,
|
||||
required this.type,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
final CalculationType type;
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
height: GridSize.popoverItemHeight,
|
||||
child: FlowyButton(
|
||||
text: FlowyText.medium(type.label, overflow: TextOverflow.ellipsis),
|
||||
onTap: () {
|
||||
onTap();
|
||||
PopoverContainer.of(context).close();
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/plugins/database/grid/application/calculations/calculations_bloc.dart';
|
||||
import 'package:appflowy/plugins/database/grid/application/grid_bloc.dart';
|
||||
import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';
|
||||
import 'package:appflowy/plugins/database/grid/presentation/widgets/calculations/calculate_cell.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class GridCalculationsRow extends StatelessWidget {
|
||||
const GridCalculationsRow({super.key, required this.viewId});
|
||||
|
||||
final String viewId;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final gridBloc = context.read<GridBloc>();
|
||||
|
||||
return BlocProvider(
|
||||
create: (context) => CalculationsBloc(
|
||||
viewId: gridBloc.databaseController.viewId,
|
||||
fieldController: gridBloc.databaseController.fieldController,
|
||||
)..add(const CalculationsEvent.started()),
|
||||
child: BlocBuilder<CalculationsBloc, CalculationsState>(
|
||||
builder: (context, state) {
|
||||
return Padding(
|
||||
padding: GridSize.contentInsets,
|
||||
child: Row(
|
||||
children: [
|
||||
...state.fields.map(
|
||||
(field) => CalculateCell(
|
||||
key: Key(
|
||||
'${field.id}-${state.calculationsByFieldId[field.id]?.id}',
|
||||
),
|
||||
width: field.fieldSettings!.width.toDouble(),
|
||||
fieldInfo: field,
|
||||
calculation: state.calculationsByFieldId[field.id],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/button.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||
|
||||
class RemoveCalculationButton extends StatelessWidget {
|
||||
const RemoveCalculationButton({
|
||||
super.key,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
height: GridSize.popoverItemHeight,
|
||||
child: FlowyButton(
|
||||
text: FlowyText.medium(
|
||||
LocaleKeys.grid_calculationTypeLabel_none.tr(),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
onTap: () {
|
||||
onTap();
|
||||
PopoverContainer.of(context).close();
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+6
-1
@@ -35,7 +35,12 @@ class FilterMenu extends StatelessWidget {
|
||||
final List<Widget> children = [];
|
||||
children.addAll(
|
||||
state.filters
|
||||
.map((filterInfo) => FilterMenuItem(filterInfo: filterInfo))
|
||||
.map(
|
||||
(filterInfo) => FilterMenuItem(
|
||||
key: ValueKey(filterInfo.filter.id),
|
||||
filterInfo: filterInfo,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
);
|
||||
|
||||
|
||||
+9
-3
@@ -1,13 +1,13 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/database/grid/application/grid_bloc.dart';
|
||||
import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/theme_extension.dart';
|
||||
|
||||
import 'package:flowy_infra_ui/style_widget/button.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class GridAddRowButton extends StatelessWidget {
|
||||
@@ -16,6 +16,12 @@ class GridAddRowButton extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FlowyButton(
|
||||
radius: BorderRadius.zero,
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(color: Theme.of(context).dividerColor),
|
||||
),
|
||||
),
|
||||
text: FlowyText(
|
||||
LocaleKeys.grid_row_newRow.tr(),
|
||||
color: Theme.of(context).hintColor,
|
||||
@@ -38,7 +44,7 @@ class GridRowBottomBar extends StatelessWidget {
|
||||
return Container(
|
||||
padding: GridSize.footerContentInsets,
|
||||
height: GridSize.footerHeight,
|
||||
margin: const EdgeInsets.only(bottom: 8, top: 8),
|
||||
// margin: const EdgeInsets.only(bottom: 8, top: 8),
|
||||
child: const GridAddRowButton(),
|
||||
);
|
||||
}
|
||||
|
||||
+2
@@ -7,6 +7,7 @@ import 'package:appflowy/plugins/database/grid/application/grid_header_bloc.dart
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart' hide Log;
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/size.dart';
|
||||
import 'package:flowy_infra/theme_extension.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -151,6 +152,7 @@ class _CellTrailing extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: GridSize.trailHeaderPadding,
|
||||
margin: EdgeInsets.only(right: GridSize.scrollBarSize + Insets.m),
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(color: Theme.of(context).dividerColor),
|
||||
|
||||
+7
-7
@@ -1,8 +1,8 @@
|
||||
import 'package:appflowy/plugins/database/application/cell/cell_controller.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import "package:appflowy/generated/locale_keys.g.dart";
|
||||
import 'package:appflowy/plugins/database/application/cell/cell_controller.dart';
|
||||
import 'package:appflowy/plugins/database/application/field/field_controller.dart';
|
||||
import 'package:appflowy/plugins/database/application/row/row_controller.dart';
|
||||
import 'package:appflowy/plugins/database/application/row/row_service.dart';
|
||||
@@ -326,6 +326,12 @@ class _RowEnterRegionState extends State<_RowEnterRegion> {
|
||||
_rowStateNotifier = RegionStateNotifier();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> dispose() async {
|
||||
_rowStateNotifier.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ChangeNotifierProvider.value(
|
||||
@@ -338,10 +344,4 @@ class _RowEnterRegionState extends State<_RowEnterRegion> {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> dispose() async {
|
||||
_rowStateNotifier.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
+68
-22
@@ -1,3 +1,5 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/database/application/field/field_controller.dart';
|
||||
@@ -48,33 +50,58 @@ class _SortEditorState extends State<SortEditor> {
|
||||
)..add(const SortEditorEvent.initial()),
|
||||
child: BlocBuilder<SortEditorBloc, SortEditorState>(
|
||||
builder: (context, state) {
|
||||
return Column(
|
||||
children: [
|
||||
...state.sortInfos.map(
|
||||
(info) => Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 6),
|
||||
child: DatabaseSortItem(
|
||||
sortInfo: info,
|
||||
popoverMutex: popoverMutex,
|
||||
),
|
||||
),
|
||||
final sortInfos = state.sortInfos;
|
||||
|
||||
return ReorderableListView.builder(
|
||||
onReorder: (oldIndex, newIndex) => context
|
||||
.read<SortEditorBloc>()
|
||||
.add(SortEditorEvent.reorderSort(oldIndex, newIndex)),
|
||||
itemCount: state.sortInfos.length,
|
||||
itemBuilder: (context, index) => Padding(
|
||||
key: ValueKey(sortInfos[index].sortId),
|
||||
padding: const EdgeInsets.symmetric(vertical: 6),
|
||||
child: DatabaseSortItem(
|
||||
index: index,
|
||||
sortInfo: sortInfos[index],
|
||||
popoverMutex: popoverMutex,
|
||||
),
|
||||
Row(
|
||||
),
|
||||
proxyDecorator: (child, index, animation) => Material(
|
||||
color: Colors.transparent,
|
||||
child: Stack(
|
||||
children: [
|
||||
Flexible(
|
||||
child: DatabaseAddSortButton(
|
||||
viewId: widget.viewId,
|
||||
fieldController: widget.fieldController,
|
||||
popoverMutex: popoverMutex,
|
||||
),
|
||||
BlocProvider.value(
|
||||
value: context.read<SortEditorBloc>(),
|
||||
child: child,
|
||||
),
|
||||
const HSpace(6),
|
||||
Flexible(
|
||||
child: DatabaseDeleteSortButton(popoverMutex: popoverMutex),
|
||||
MouseRegion(
|
||||
cursor: Platform.isWindows
|
||||
? SystemMouseCursors.click
|
||||
: SystemMouseCursors.grabbing,
|
||||
child: const SizedBox.expand(),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
shrinkWrap: true,
|
||||
buildDefaultDragHandles: false,
|
||||
footer: Row(
|
||||
children: [
|
||||
Flexible(
|
||||
child: DatabaseAddSortButton(
|
||||
viewId: widget.viewId,
|
||||
fieldController: widget.fieldController,
|
||||
popoverMutex: popoverMutex,
|
||||
),
|
||||
),
|
||||
const HSpace(6),
|
||||
Flexible(
|
||||
child: DatabaseDeleteSortButton(
|
||||
popoverMutex: popoverMutex,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
@@ -85,10 +112,12 @@ class _SortEditorState extends State<SortEditor> {
|
||||
class DatabaseSortItem extends StatelessWidget {
|
||||
const DatabaseSortItem({
|
||||
super.key,
|
||||
required this.index,
|
||||
required this.popoverMutex,
|
||||
required this.sortInfo,
|
||||
});
|
||||
|
||||
final int index;
|
||||
final PopoverMutex popoverMutex;
|
||||
final SortInfo sortInfo;
|
||||
|
||||
@@ -107,6 +136,23 @@ class DatabaseSortItem extends StatelessWidget {
|
||||
|
||||
return Row(
|
||||
children: [
|
||||
ReorderableDragStartListener(
|
||||
index: index,
|
||||
child: MouseRegion(
|
||||
cursor: Platform.isWindows
|
||||
? SystemMouseCursors.click
|
||||
: SystemMouseCursors.grab,
|
||||
child: SizedBox(
|
||||
width: 14,
|
||||
height: 14,
|
||||
child: FlowySvg(
|
||||
FlowySvgs.drag_element_s,
|
||||
color: Theme.of(context).iconTheme.color,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const HSpace(6),
|
||||
SizedBox(
|
||||
height: 26,
|
||||
child: SortChoiceButton(
|
||||
@@ -122,8 +168,8 @@ class DatabaseSortItem extends StatelessWidget {
|
||||
popoverMutex: popoverMutex,
|
||||
),
|
||||
),
|
||||
const HSpace(6),
|
||||
const Spacer(),
|
||||
const HSpace(6),
|
||||
deleteButton,
|
||||
],
|
||||
);
|
||||
|
||||
+17
-19
@@ -31,27 +31,25 @@ class SortMenu extends StatelessWidget {
|
||||
)..add(const SortMenuEvent.initial()),
|
||||
child: BlocBuilder<SortMenuBloc, SortMenuState>(
|
||||
builder: (context, state) {
|
||||
if (state.sortInfos.isNotEmpty) {
|
||||
return AppFlowyPopover(
|
||||
controller: PopoverController(),
|
||||
constraints: BoxConstraints.loose(const Size(320, 200)),
|
||||
direction: PopoverDirection.bottomWithLeftAligned,
|
||||
offset: const Offset(0, 5),
|
||||
popupBuilder: (BuildContext popoverContext) {
|
||||
return SingleChildScrollView(
|
||||
child: SortEditor(
|
||||
viewId: state.viewId,
|
||||
fieldController:
|
||||
context.read<SortMenuBloc>().fieldController,
|
||||
sortInfos: state.sortInfos,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: SortChoiceChip(sortInfos: state.sortInfos),
|
||||
);
|
||||
if (state.sortInfos.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return const SizedBox.shrink();
|
||||
return AppFlowyPopover(
|
||||
controller: PopoverController(),
|
||||
constraints: BoxConstraints.loose(const Size(320, 200)),
|
||||
direction: PopoverDirection.bottomWithLeftAligned,
|
||||
offset: const Offset(0, 5),
|
||||
margin: const EdgeInsets.fromLTRB(6.0, 0.0, 6.0, 6.0),
|
||||
popupBuilder: (BuildContext popoverContext) {
|
||||
return SortEditor(
|
||||
viewId: state.viewId,
|
||||
fieldController: context.read<SortMenuBloc>().fieldController,
|
||||
sortInfos: state.sortInfos,
|
||||
);
|
||||
},
|
||||
child: SortChoiceChip(sortInfos: state.sortInfos),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
+65
-114
@@ -1,7 +1,11 @@
|
||||
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/database/view/database_view_list.dart';
|
||||
import 'package:appflowy/plugins/base/emoji/emoji_text.dart';
|
||||
import 'package:appflowy/plugins/database/application/tab_bar_bloc.dart';
|
||||
import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/setting/mobile_database_controls.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_ext.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
@@ -24,10 +28,11 @@ class _MobileTabBarHeaderState extends State<MobileTabBarHeader> {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Expanded(child: _DatabaseViewList()),
|
||||
const HSpace(10),
|
||||
HSpace(GridSize.leadingHeaderPadding),
|
||||
const _DatabaseViewSelectorButton(),
|
||||
const Spacer(),
|
||||
BlocBuilder<DatabaseTabBarBloc, DatabaseTabBarState>(
|
||||
builder: (context, state) {
|
||||
final currentView = state.tabBars.firstWhereIndexedOrNull(
|
||||
@@ -51,134 +56,80 @@ class _MobileTabBarHeaderState extends State<MobileTabBarHeader> {
|
||||
}
|
||||
}
|
||||
|
||||
class _DatabaseViewList extends StatelessWidget {
|
||||
const _DatabaseViewList();
|
||||
class _DatabaseViewSelectorButton extends StatelessWidget {
|
||||
const _DatabaseViewSelectorButton();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<DatabaseTabBarBloc, DatabaseTabBarState>(
|
||||
builder: (context, state) {
|
||||
final currentView = state.tabBars.firstWhereIndexedOrNull(
|
||||
final tabBar = state.tabBars.firstWhereIndexedOrNull(
|
||||
(index, tabBar) => index == state.selectedIndex,
|
||||
);
|
||||
|
||||
if (currentView == null) {
|
||||
if (tabBar == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
final children = state.tabBars.mapIndexed<Widget>((index, tabBar) {
|
||||
return Padding(
|
||||
padding: EdgeInsetsDirectional.only(
|
||||
start: index == 0 ? 0 : 2,
|
||||
end: 2,
|
||||
return TextButton(
|
||||
style: ButtonStyle(
|
||||
padding: const MaterialStatePropertyAll(
|
||||
EdgeInsets.fromLTRB(12, 8, 8, 8),
|
||||
),
|
||||
child: _DatabaseViewListItem(
|
||||
tabBar: tabBar,
|
||||
isSelected: currentView.viewId == tabBar.viewId,
|
||||
maximumSize: const MaterialStatePropertyAll(Size(200, 48)),
|
||||
minimumSize: const MaterialStatePropertyAll(Size(48, 0)),
|
||||
shape: const MaterialStatePropertyAll(
|
||||
RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(12)),
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList();
|
||||
|
||||
children.insert(0, HSpace(GridSize.leadingHeaderPadding));
|
||||
|
||||
return SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(children: children),
|
||||
backgroundColor: const MaterialStatePropertyAll(Color(0x0F212729)),
|
||||
overlayColor: MaterialStatePropertyAll(
|
||||
Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_buildViewIconButton(context, tabBar.view),
|
||||
const HSpace(6),
|
||||
Flexible(
|
||||
child: FlowyText.medium(
|
||||
tabBar.view.name,
|
||||
fontSize: 13,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
const HSpace(8),
|
||||
const FlowySvg(
|
||||
FlowySvgs.arrow_tight_s,
|
||||
size: Size.square(10),
|
||||
),
|
||||
],
|
||||
),
|
||||
onPressed: () {
|
||||
showMobileBottomSheet(
|
||||
context,
|
||||
showDivider: false,
|
||||
builder: (_) {
|
||||
return MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider<ViewBloc>.value(
|
||||
value: context.read<ViewBloc>(),
|
||||
),
|
||||
BlocProvider<DatabaseTabBarBloc>.value(
|
||||
value: context.read<DatabaseTabBarBloc>(),
|
||||
),
|
||||
],
|
||||
child: const MobileDatabaseViewList(),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _DatabaseViewListItem extends StatefulWidget {
|
||||
const _DatabaseViewListItem({
|
||||
required this.tabBar,
|
||||
required this.isSelected,
|
||||
});
|
||||
|
||||
final DatabaseTabBar tabBar;
|
||||
final bool isSelected;
|
||||
|
||||
@override
|
||||
State<_DatabaseViewListItem> createState() => _DatabaseViewListItemState();
|
||||
}
|
||||
|
||||
class _DatabaseViewListItemState extends State<_DatabaseViewListItem> {
|
||||
late final MaterialStatesController statesController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
statesController = MaterialStatesController(
|
||||
<MaterialState>{if (widget.isSelected) MaterialState.selected},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (widget.isSelected != oldWidget.isSelected) {
|
||||
statesController.update(MaterialState.selected, widget.isSelected);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return TextButton(
|
||||
statesController: statesController,
|
||||
style: ButtonStyle(
|
||||
padding: const MaterialStatePropertyAll(
|
||||
EdgeInsets.symmetric(horizontal: 12, vertical: 7),
|
||||
),
|
||||
maximumSize: MaterialStateProperty.resolveWith((states) {
|
||||
if (states.contains(MaterialState.selected)) {
|
||||
return const Size(150, 48);
|
||||
}
|
||||
return const Size(120, 48);
|
||||
}),
|
||||
minimumSize: const MaterialStatePropertyAll(Size(48, 0)),
|
||||
shape: const MaterialStatePropertyAll(
|
||||
RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(12)),
|
||||
),
|
||||
),
|
||||
backgroundColor: MaterialStateProperty.resolveWith((states) {
|
||||
if (states.contains(MaterialState.selected)) {
|
||||
return const Color(0x0F212729);
|
||||
}
|
||||
return Colors.transparent;
|
||||
}),
|
||||
overlayColor: MaterialStateProperty.resolveWith((states) {
|
||||
if (states.contains(MaterialState.selected)) {
|
||||
return Colors.transparent;
|
||||
}
|
||||
return Theme.of(context).colorScheme.secondary;
|
||||
}),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_buildViewIconButton(context, widget.tabBar.view),
|
||||
const HSpace(6),
|
||||
Flexible(
|
||||
child: FlowyText(
|
||||
widget.tabBar.view.name,
|
||||
fontSize: 14,
|
||||
fontWeight: widget.isSelected ? FontWeight.w500 : FontWeight.w400,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
onPressed: () {
|
||||
if (!widget.isSelected) {
|
||||
context
|
||||
.read<DatabaseTabBarBloc>()
|
||||
.add(DatabaseTabBarEvent.selectView(widget.tabBar.viewId));
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildViewIconButton(BuildContext context, ViewPB view) {
|
||||
return view.icon.value.isNotEmpty
|
||||
|
||||
@@ -41,6 +41,12 @@ abstract class DatabaseTabBarItemBuilder {
|
||||
BuildContext context,
|
||||
DatabaseController controller,
|
||||
);
|
||||
|
||||
/// Should be called in case a builder has resources it
|
||||
/// needs to dispose of.
|
||||
///
|
||||
// If we add any logic in this method, add @mustCallSuper !
|
||||
void dispose() {}
|
||||
}
|
||||
|
||||
class DatabaseTabBarView extends StatefulWidget {
|
||||
|
||||
@@ -47,6 +47,16 @@ class CardBloc extends Bloc<CardEvent, CardState> {
|
||||
|
||||
VoidCallback? _rowCallback;
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
if (_rowCallback != null) {
|
||||
_rowCache.removeRowListener(_rowCallback!);
|
||||
_rowCallback = null;
|
||||
}
|
||||
await _rowListener.stop();
|
||||
return super.close();
|
||||
}
|
||||
|
||||
void _dispatch() {
|
||||
on<CardEvent>(
|
||||
(event, emit) async {
|
||||
@@ -73,16 +83,6 @@ class CardBloc extends Bloc<CardEvent, CardState> {
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
if (_rowCallback != null) {
|
||||
_rowCache.removeRowListener(_rowCallback!);
|
||||
_rowCallback = null;
|
||||
}
|
||||
await _rowListener.stop();
|
||||
return super.close();
|
||||
}
|
||||
|
||||
Future<void> _startListening() async {
|
||||
_rowCallback = _rowCache.addListener(
|
||||
rowId: rowId,
|
||||
@@ -113,7 +113,7 @@ List<CellContext> _makeCells(
|
||||
cellContexts.removeWhere((cellContext) {
|
||||
final fieldInfo = fieldController.getField(cellContext.fieldId);
|
||||
return fieldInfo == null ||
|
||||
!fieldInfo.fieldSettings!.visibility.isVisibleState() ||
|
||||
!(fieldInfo.fieldSettings?.visibility.isVisibleState() ?? false) ||
|
||||
(groupFieldId != null && cellContext.fieldId == groupFieldId);
|
||||
});
|
||||
return cellContexts.toList();
|
||||
|
||||
+16
-22
@@ -10,7 +10,7 @@ import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../editable_cell_skeleton/checklist.dart';
|
||||
|
||||
@@ -113,40 +113,34 @@ class _ChecklistItemsState extends State<ChecklistItems> {
|
||||
),
|
||||
const VSpace(4),
|
||||
...children,
|
||||
const ChecklistItemControl(),
|
||||
ChecklistItemControl(cellNotifer: widget.cellContainerNotifier),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ChecklistItemControl extends StatefulWidget {
|
||||
const ChecklistItemControl({super.key});
|
||||
class ChecklistItemControl extends StatelessWidget {
|
||||
const ChecklistItemControl({super.key, required this.cellNotifer});
|
||||
|
||||
@override
|
||||
State<ChecklistItemControl> createState() => _ChecklistItemControlState();
|
||||
}
|
||||
|
||||
class _ChecklistItemControlState extends State<ChecklistItemControl> {
|
||||
bool _isHover = false;
|
||||
final CellContainerNotifier cellNotifer;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MouseRegion(
|
||||
onHover: (_) => setState(() => _isHover = true),
|
||||
onExit: (_) => setState(() => _isHover = false),
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () => context
|
||||
.read<ChecklistCellBloc>()
|
||||
.add(const ChecklistCellEvent.createNewTask("")),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(8.0, 4.0, 8.0, 0),
|
||||
child: SizedBox(
|
||||
return ChangeNotifierProvider.value(
|
||||
value: cellNotifer,
|
||||
child: Consumer<CellContainerNotifier>(
|
||||
builder: (buildContext, notifier, _) => GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () => context
|
||||
.read<ChecklistCellBloc>()
|
||||
.add(const ChecklistCellEvent.createNewTask("")),
|
||||
child: Container(
|
||||
margin: const EdgeInsets.fromLTRB(8.0, 4.0, 8.0, 0),
|
||||
height: 12,
|
||||
child: AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 150),
|
||||
child: _isHover
|
||||
child: notifier.isHover
|
||||
? FlowyTooltip(
|
||||
message: LocaleKeys.grid_checklist_addNew.tr(),
|
||||
child: Row(
|
||||
|
||||
+29
-46
@@ -1,10 +1,14 @@
|
||||
import 'package:appflowy/plugins/database/application/cell/cell_controller.dart';
|
||||
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
|
||||
import 'package:appflowy/plugins/database/application/database_controller.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
import 'package:appflowy/plugins/database/application/cell/cell_controller.dart';
|
||||
import 'package:appflowy/plugins/database/application/database_controller.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||
|
||||
import '../row/accessory/cell_accessory.dart';
|
||||
import '../row/accessory/cell_shortcuts.dart';
|
||||
import '../row/cells/cell_container.dart';
|
||||
|
||||
import 'editable_cell_skeleton/checkbox.dart';
|
||||
import 'editable_cell_skeleton/checklist.dart';
|
||||
import 'editable_cell_skeleton/date.dart';
|
||||
@@ -13,9 +17,6 @@ import 'editable_cell_skeleton/select_option.dart';
|
||||
import 'editable_cell_skeleton/text.dart';
|
||||
import 'editable_cell_skeleton/timestamp.dart';
|
||||
import 'editable_cell_skeleton/url.dart';
|
||||
import '../row/accessory/cell_accessory.dart';
|
||||
import '../row/accessory/cell_shortcuts.dart';
|
||||
import '../row/cells/cell_container.dart';
|
||||
|
||||
enum EditableCellStyle {
|
||||
desktopGrid,
|
||||
@@ -113,11 +114,12 @@ class EditableCellBuilder {
|
||||
CellContext cellContext, {
|
||||
required EditableCellSkinMap skinMap,
|
||||
}) {
|
||||
final cellController = makeCellController(databaseController, cellContext);
|
||||
final DatabaseController(:fieldController) = databaseController;
|
||||
final fieldType = fieldController.getField(cellContext.fieldId)!.fieldType;
|
||||
|
||||
final key = ValueKey(
|
||||
"${databaseController.viewId}${cellContext.fieldId}${cellContext.rowId}",
|
||||
);
|
||||
final fieldType = cellController.fieldType;
|
||||
assert(skinMap.has(fieldType));
|
||||
return switch (fieldType) {
|
||||
FieldType.Checkbox => EditableCheckboxCell(
|
||||
@@ -190,11 +192,9 @@ class EditableCellBuilder {
|
||||
}
|
||||
|
||||
abstract class CellEditable {
|
||||
RequestFocusListener get requestFocus;
|
||||
SingleListenerChangeNotifier get requestFocus;
|
||||
|
||||
CellContainerNotifier get cellContainerNotifier;
|
||||
|
||||
// ValueNotifier<bool> get onCellEditing;
|
||||
}
|
||||
|
||||
typedef AccessoryBuilder = List<GridCellAccessoryBuilder> Function(
|
||||
@@ -204,9 +204,6 @@ typedef AccessoryBuilder = List<GridCellAccessoryBuilder> Function(
|
||||
abstract class CellAccessory extends Widget {
|
||||
const CellAccessory({super.key});
|
||||
|
||||
// The hover will show if the isHover's value is true
|
||||
ValueNotifier<bool>? get onAccessoryHover;
|
||||
|
||||
AccessoryBuilder? get accessoryBuilder;
|
||||
}
|
||||
|
||||
@@ -217,20 +214,11 @@ abstract class EditableCellWidget extends StatefulWidget
|
||||
@override
|
||||
final CellContainerNotifier cellContainerNotifier = CellContainerNotifier();
|
||||
|
||||
// When the cell is focused, we assume that the accessory also be hovered.
|
||||
@override
|
||||
ValueNotifier<bool> get onAccessoryHover => ValueNotifier(false);
|
||||
|
||||
// @override
|
||||
// final ValueNotifier<bool> onCellEditing = ValueNotifier<bool>(false);
|
||||
AccessoryBuilder? get accessoryBuilder => null;
|
||||
|
||||
@override
|
||||
List<GridCellAccessoryBuilder> Function(
|
||||
GridCellAccessoryBuildContext buildContext,
|
||||
)? get accessoryBuilder => null;
|
||||
|
||||
@override
|
||||
final RequestFocusListener requestFocus = RequestFocusListener();
|
||||
final requestFocus = SingleListenerChangeNotifier();
|
||||
|
||||
@override
|
||||
final Map<CellKeyboardKey, CellKeyboardAction> shortcutHandlers = {};
|
||||
@@ -240,28 +228,26 @@ abstract class GridCellState<T extends EditableCellWidget> extends State<T> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
widget.requestFocus.setListener(requestBeginFocus);
|
||||
widget.requestFocus.addListener(onRequestFocus);
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant T oldWidget) {
|
||||
if (oldWidget != this) {
|
||||
widget.requestFocus.setListener(requestBeginFocus);
|
||||
widget.requestFocus.addListener(onRequestFocus);
|
||||
}
|
||||
super.didUpdateWidget(oldWidget);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
widget.onAccessoryHover.dispose();
|
||||
widget.requestFocus.removeAllListener();
|
||||
widget.requestFocus.removeListener(onRequestFocus);
|
||||
widget.requestFocus.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
/// Subclass can override this method to request focus.
|
||||
void requestBeginFocus();
|
||||
void onRequestFocus();
|
||||
|
||||
String? onCopy() => null;
|
||||
}
|
||||
@@ -287,7 +273,7 @@ abstract class GridEditableTextCell<T extends EditableCellWidget>
|
||||
}
|
||||
|
||||
@override
|
||||
void requestBeginFocus() {
|
||||
void onRequestFocus() {
|
||||
if (!focusNode.hasFocus && focusNode.canRequestFocus) {
|
||||
FocusScope.of(context).requestFocus(focusNode);
|
||||
}
|
||||
@@ -304,28 +290,25 @@ abstract class GridEditableTextCell<T extends EditableCellWidget>
|
||||
Future<void> focusChanged() async {}
|
||||
}
|
||||
|
||||
class RequestFocusListener extends ChangeNotifier {
|
||||
class SingleListenerChangeNotifier extends ChangeNotifier {
|
||||
VoidCallback? _listener;
|
||||
|
||||
void setListener(VoidCallback listener) {
|
||||
@override
|
||||
void addListener(VoidCallback listener) {
|
||||
if (_listener != null) {
|
||||
removeListener(_listener!);
|
||||
}
|
||||
|
||||
_listener = listener;
|
||||
addListener(listener);
|
||||
super.addListener(listener);
|
||||
}
|
||||
|
||||
void removeAllListener() {
|
||||
if (_listener != null) {
|
||||
removeListener(_listener!);
|
||||
_listener = null;
|
||||
}
|
||||
@override
|
||||
void dispose() {
|
||||
_listener = null;
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void notify() {
|
||||
notifyListeners();
|
||||
}
|
||||
void notify() => notifyListeners();
|
||||
}
|
||||
|
||||
class SingleListenerFocusNode extends FocusNode {
|
||||
@@ -374,7 +357,7 @@ class EditableCellSkinMap {
|
||||
FieldType.Checklist => checklistSkin != null,
|
||||
FieldType.CreatedTime ||
|
||||
FieldType.LastEditedTime =>
|
||||
throw timestampSkin != null,
|
||||
timestampSkin != null,
|
||||
FieldType.DateTime => dateSkin != null,
|
||||
FieldType.MultiSelect ||
|
||||
FieldType.SingleSelect =>
|
||||
|
||||
+1
-3
@@ -80,9 +80,7 @@ class _CheckboxCellState extends GridCellState<EditableCheckboxCell> {
|
||||
}
|
||||
|
||||
@override
|
||||
void requestBeginFocus() {
|
||||
cellBloc.add(const CheckboxCellEvent.select());
|
||||
}
|
||||
void onRequestFocus() => cellBloc.add(const CheckboxCellEvent.select());
|
||||
|
||||
@override
|
||||
String? onCopy() {
|
||||
|
||||
+1
-1
@@ -85,7 +85,7 @@ class GridChecklistCellState extends GridCellState<EditableChecklistCell> {
|
||||
}
|
||||
|
||||
@override
|
||||
void requestBeginFocus() {
|
||||
void onRequestFocus() {
|
||||
if (widget.skin is DesktopGridChecklistCellSkin) {
|
||||
_popover.show();
|
||||
}
|
||||
|
||||
+1
-1
@@ -84,7 +84,7 @@ class _DateCellState extends GridCellState<EditableDateCell> {
|
||||
}
|
||||
|
||||
@override
|
||||
void requestBeginFocus() {
|
||||
void onRequestFocus() {
|
||||
_popover.show();
|
||||
widget.cellContainerNotifier.isFocus = true;
|
||||
}
|
||||
|
||||
+1
-1
@@ -96,7 +96,7 @@ class _NumberCellState extends GridEditableTextCell<EditableNumberCell> {
|
||||
SingleListenerFocusNode focusNode = SingleListenerFocusNode();
|
||||
|
||||
@override
|
||||
void requestBeginFocus() {
|
||||
void onRequestFocus() {
|
||||
focusNode.requestFocus();
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -92,5 +92,5 @@ class _SelectOptionCellState extends GridCellState<EditableSelectOptionCell> {
|
||||
}
|
||||
|
||||
@override
|
||||
void requestBeginFocus() => _popover.show();
|
||||
void onRequestFocus() => _popover.show();
|
||||
}
|
||||
+1
-1
@@ -97,7 +97,7 @@ class _TextCellState extends GridEditableTextCell<EditableTextCell> {
|
||||
SingleListenerFocusNode focusNode = SingleListenerFocusNode();
|
||||
|
||||
@override
|
||||
void requestBeginFocus() {
|
||||
void onRequestFocus() {
|
||||
focusNode.requestFocus();
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -83,7 +83,7 @@ class _TimestampCellState extends GridCellState<EditableTimestampCell> {
|
||||
}
|
||||
|
||||
@override
|
||||
void requestBeginFocus() {
|
||||
void onRequestFocus() {
|
||||
widget.cellContainerNotifier.isFocus = true;
|
||||
}
|
||||
|
||||
|
||||
+1
@@ -89,6 +89,7 @@ class _GridURLCellState extends GridEditableTextCell<EditableURLCell> {
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
widget._cellDataNotifier.dispose();
|
||||
_textEditingController.dispose();
|
||||
cellBloc.close();
|
||||
super.dispose();
|
||||
|
||||
+2
-2
@@ -78,13 +78,13 @@ class MobileRowDetailURLCellSkin extends IEditableURLCellSkin {
|
||||
const [];
|
||||
|
||||
void _showURLEditor(BuildContext context, URLCellBloc bloc, String content) {
|
||||
final controller = TextEditingController(text: content);
|
||||
showMobileBottomSheet(
|
||||
context,
|
||||
title: LocaleKeys.board_mobile_editURL.tr(),
|
||||
showHeader: true,
|
||||
showCloseButton: true,
|
||||
builder: (_) {
|
||||
final controller = TextEditingController(text: content);
|
||||
return TextField(
|
||||
controller: controller,
|
||||
autofocus: true,
|
||||
@@ -95,6 +95,6 @@ class MobileRowDetailURLCellSkin extends IEditableURLCellSkin {
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
).then((_) => controller.dispose());
|
||||
}
|
||||
}
|
||||
+14
@@ -184,6 +184,14 @@ class _ChecklistItemState extends State<ChecklistItem> {
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_textController.dispose();
|
||||
_focusNode.dispose();
|
||||
_debounceOnChanged?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(ChecklistItem oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
@@ -300,6 +308,12 @@ class _NewTaskItemState extends State<NewTaskItem> {
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_textEditingController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
|
||||
+1
@@ -37,6 +37,7 @@ class _SelectOptionCellEditorState extends State<SelectOptionCellEditor> {
|
||||
@override
|
||||
void dispose() {
|
||||
popoverMutex.dispose();
|
||||
textEditingController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
||||
-1
@@ -162,7 +162,6 @@ class _SelectOptionTextFieldState extends State<SelectOptionTextField> {
|
||||
},
|
||||
),
|
||||
child: SingleChildScrollView(
|
||||
controller: ScrollController(),
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Wrap(spacing: 4, children: children),
|
||||
),
|
||||
|
||||
+5
-5
@@ -98,7 +98,7 @@ class _GridCellEnterRegion extends StatelessWidget {
|
||||
return Selector2<RegionStateNotifier, CellContainerNotifier, bool>(
|
||||
selector: (context, regionNotifier, cellNotifier) =>
|
||||
!cellNotifier.isFocus &&
|
||||
(cellNotifier.onEnter || regionNotifier.onEnter && isPrimary),
|
||||
(cellNotifier.isHover || regionNotifier.onEnter && isPrimary),
|
||||
builder: (context, showAccessory, _) {
|
||||
final List<Widget> children = [child];
|
||||
|
||||
@@ -113,9 +113,9 @@ class _GridCellEnterRegion extends StatelessWidget {
|
||||
return MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
onEnter: (p) =>
|
||||
CellContainerNotifier.of(context, listen: false).onEnter = true,
|
||||
CellContainerNotifier.of(context, listen: false).isHover = true,
|
||||
onExit: (p) =>
|
||||
CellContainerNotifier.of(context, listen: false).onEnter = false,
|
||||
CellContainerNotifier.of(context, listen: false).isHover = false,
|
||||
child: Stack(
|
||||
alignment: AlignmentDirectional.center,
|
||||
fit: StackFit.expand,
|
||||
@@ -138,7 +138,7 @@ class CellContainerNotifier extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
set onEnter(bool value) {
|
||||
set isHover(bool value) {
|
||||
if (_onEnter != value) {
|
||||
_onEnter = value;
|
||||
notifyListeners();
|
||||
@@ -147,7 +147,7 @@ class CellContainerNotifier extends ChangeNotifier {
|
||||
|
||||
bool get isFocus => _isFocus;
|
||||
|
||||
bool get onEnter => _onEnter;
|
||||
bool get isHover => _onEnter;
|
||||
|
||||
static CellContainerNotifier of(BuildContext context, {bool listen = true}) {
|
||||
return Provider.of<CellContainerNotifier>(context, listen: listen);
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/plugins/database/application/database_controller.dart';
|
||||
import 'package:appflowy/plugins/database/application/row/row_controller.dart';
|
||||
import 'package:appflowy/plugins/database/grid/application/row/row_detail_bloc.dart';
|
||||
@@ -5,10 +7,10 @@ import 'package:appflowy/plugins/database/widgets/row/row_document.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/user/application/reminder/reminder_bloc.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import '../cell/editable_cell_builder.dart';
|
||||
|
||||
import 'row_banner.dart';
|
||||
import 'row_property.dart';
|
||||
|
||||
@@ -49,9 +51,7 @@ class _RowDetailPageState extends State<RowDetailPage> {
|
||||
rowController: widget.rowController,
|
||||
),
|
||||
),
|
||||
BlocProvider.value(
|
||||
value: getIt<ReminderBloc>(),
|
||||
),
|
||||
BlocProvider.value(value: getIt<ReminderBloc>()),
|
||||
],
|
||||
child: ListView(
|
||||
controller: scrollController,
|
||||
|
||||
@@ -189,8 +189,14 @@ class _PropertyCellState extends State<_PropertyCell> {
|
||||
margin: const EdgeInsets.only(bottom: 8),
|
||||
constraints: const BoxConstraints(minHeight: 30),
|
||||
child: MouseRegion(
|
||||
onEnter: (event) => _isFieldHover.value = true,
|
||||
onExit: (event) => _isFieldHover.value = false,
|
||||
onEnter: (event) {
|
||||
_isFieldHover.value = true;
|
||||
cell.cellContainerNotifier.isHover = true;
|
||||
},
|
||||
onExit: (event) {
|
||||
_isFieldHover.value = false;
|
||||
cell.cellContainerNotifier.isHover = false;
|
||||
},
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
|
||||
+21
-59
@@ -1,10 +1,8 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
|
||||
import 'package:appflowy/mobile/presentation/database/view/database_view_list.dart';
|
||||
import 'package:appflowy/mobile/presentation/database/view/edit_database_view_screen.dart';
|
||||
import 'package:appflowy/mobile/presentation/database/view/database_field_list.dart';
|
||||
import 'package:appflowy/plugins/database/application/database_controller.dart';
|
||||
import 'package:appflowy/plugins/database/application/tab_bar_bloc.dart';
|
||||
import 'package:appflowy/plugins/database/grid/application/filter/filter_menu_bloc.dart';
|
||||
import 'package:appflowy/plugins/database/grid/application/sort/sort_menu_bloc.dart';
|
||||
import 'package:appflowy/plugins/database/grid/presentation/grid_page.dart';
|
||||
@@ -58,62 +56,26 @@ class MobileDatabaseControls extends StatelessWidget {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return Row(
|
||||
children: [
|
||||
_DatabaseControlButton(
|
||||
icon: FlowySvgs.settings_s,
|
||||
onTap: () {
|
||||
showMobileBottomSheet(
|
||||
context,
|
||||
showHeader: true,
|
||||
showDoneButton: true,
|
||||
title: LocaleKeys.grid_settings_editView.tr(),
|
||||
enableDraggableScrollable: true,
|
||||
initialChildSize: 0.98,
|
||||
minChildSize: 0.98,
|
||||
maxChildSize: 0.98,
|
||||
builder: (_) {
|
||||
return BlocProvider<ViewBloc>(
|
||||
create: (_) {
|
||||
return ViewBloc(
|
||||
view: context
|
||||
.read<DatabaseTabBarBloc>()
|
||||
.state
|
||||
.tabBarControllerByViewId[controller.viewId]!
|
||||
.view,
|
||||
)..add(const ViewEvent.initial());
|
||||
},
|
||||
child: MobileEditDatabaseViewScreen(
|
||||
databaseController: controller,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
_DatabaseControlButton(
|
||||
icon: FlowySvgs.align_left_s,
|
||||
onTap: () {
|
||||
showMobileBottomSheet(
|
||||
context,
|
||||
showDivider: false,
|
||||
builder: (_) {
|
||||
return MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider<ViewBloc>.value(
|
||||
value: context.read<ViewBloc>(),
|
||||
),
|
||||
BlocProvider<DatabaseTabBarBloc>.value(
|
||||
value: context.read<DatabaseTabBarBloc>(),
|
||||
),
|
||||
],
|
||||
child: const MobileDatabaseViewList(),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
return _DatabaseControlButton(
|
||||
icon: FlowySvgs.m_field_hide_s,
|
||||
onTap: () => showMobileBottomSheet(
|
||||
context,
|
||||
resizeToAvoidBottomInset: false,
|
||||
showDragHandle: true,
|
||||
showHeader: true,
|
||||
showBackButton: true,
|
||||
title: LocaleKeys.grid_settings_properties.tr(),
|
||||
showDivider: true,
|
||||
builder: (_) {
|
||||
return BlocProvider.value(
|
||||
value: context.read<ViewBloc>(),
|
||||
child: MobileDatabaseFieldList(
|
||||
databaseController: controller,
|
||||
canCreate: false,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
+5
-4
@@ -15,7 +15,6 @@ import 'package:flowy_infra/theme_extension.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
|
||||
class DatabasePropertyList extends StatefulWidget {
|
||||
const DatabasePropertyList({
|
||||
@@ -86,7 +85,7 @@ class _DatabasePropertyListState extends State<DatabasePropertyList> {
|
||||
.add(DatabasePropertyEvent.moveField(from, to));
|
||||
},
|
||||
onReorderStart: (_) => _popoverMutex.close(),
|
||||
padding: const EdgeInsets.symmetric(vertical: 6.0),
|
||||
padding: const EdgeInsets.symmetric(vertical: 4.0),
|
||||
children: cells,
|
||||
);
|
||||
},
|
||||
@@ -138,8 +137,9 @@ class _DatabasePropertyCellState extends State<DatabasePropertyCell> {
|
||||
constraints: BoxConstraints.loose(const Size(240, 400)),
|
||||
triggerActions: PopoverTriggerFlags.none,
|
||||
margin: EdgeInsets.zero,
|
||||
child: SizedBox(
|
||||
child: Container(
|
||||
height: GridSize.popoverItemHeight,
|
||||
margin: const EdgeInsets.symmetric(vertical: 2, horizontal: 6),
|
||||
child: FlowyButton(
|
||||
hoverColor: AFThemeExtension.of(context).lightGreyHover,
|
||||
text: FlowyText.medium(
|
||||
@@ -169,6 +169,7 @@ class _DatabasePropertyCellState extends State<DatabasePropertyCell> {
|
||||
FlowySvg(
|
||||
widget.fieldInfo.fieldType.svgData,
|
||||
color: Theme.of(context).iconTheme.color,
|
||||
size: const Size.square(16),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -191,7 +192,7 @@ class _DatabasePropertyCellState extends State<DatabasePropertyCell> {
|
||||
icon: visibleIcon,
|
||||
),
|
||||
onTap: () => _popoverController.show(),
|
||||
).padding(horizontal: 6.0),
|
||||
),
|
||||
),
|
||||
popupBuilder: (BuildContext context) {
|
||||
return FieldEditor(
|
||||
|
||||
@@ -8,6 +8,7 @@ import 'package:appflowy/plugins/document/document_page.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/more/cubit/document_appearance_cubit.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/more/more_button.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/share/share_button.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/favorite/favorite_button.dart';
|
||||
import 'package:appflowy/plugins/util.dart';
|
||||
import 'package:appflowy/startup/plugin/plugin.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/home_stack.dart';
|
||||
@@ -121,6 +122,11 @@ class DocumentPluginWidgetBuilder extends PluginWidgetBuilder
|
||||
children: [
|
||||
DocumentShareButton(key: ValueKey(view.id), view: view),
|
||||
const HSpace(4),
|
||||
DocumentFavoriteButton(
|
||||
key: ValueKey('favorite_button_${view.id}'),
|
||||
view: view,
|
||||
),
|
||||
const HSpace(4),
|
||||
const DocumentMoreButton(),
|
||||
],
|
||||
);
|
||||
|
||||
+8
-1
@@ -108,7 +108,8 @@ class _FindMenuState extends State<FindMenu> {
|
||||
widget.searchService.currentSelectedIndex.removeListener(_setState);
|
||||
widget.searchService.dispose();
|
||||
findTextEditingController.removeListener(_searchPattern);
|
||||
|
||||
findTextEditingController.dispose();
|
||||
findTextFieldFocusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -241,6 +242,12 @@ class _ReplaceMenuState extends State<ReplaceMenu> {
|
||||
late final FocusNode replaceTextFieldFocusNode;
|
||||
final replaceTextEditingController = TextEditingController();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
replaceTextEditingController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
|
||||
+6
-6
@@ -351,6 +351,12 @@ class CoverColorPicker extends StatefulWidget {
|
||||
class _CoverColorPickerState extends State<CoverColorPicker> {
|
||||
final scrollController = ScrollController();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
scrollController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
@@ -375,12 +381,6 @@ class _CoverColorPickerState extends State<CoverColorPicker> {
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
scrollController.dispose();
|
||||
}
|
||||
|
||||
Widget _buildColorItems(List<ColorOption> options, String? selectedColor) {
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
|
||||
+7
-3
@@ -110,9 +110,13 @@ class _NetworkImageUrlInputState extends State<NetworkImageUrlInput> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
urlController.addListener(() {
|
||||
setState(() {});
|
||||
});
|
||||
urlController.addListener(() => setState(() {}));
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
urlController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
+6
@@ -120,6 +120,12 @@ class _MathInputTextFieldState extends State<MathInputTextField> {
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
textEditingController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
|
||||
+2
-2
@@ -187,10 +187,10 @@ class MathEquationBlockComponentWidgetState
|
||||
}
|
||||
|
||||
void showEditingDialog() {
|
||||
final controller = TextEditingController(text: formula);
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
final controller = TextEditingController(text: formula);
|
||||
return AlertDialog(
|
||||
backgroundColor: Theme.of(context).canvasColor,
|
||||
title: Text(
|
||||
@@ -234,7 +234,7 @@ class MathEquationBlockComponentWidgetState
|
||||
actionsAlignment: MainAxisAlignment.spaceAround,
|
||||
);
|
||||
},
|
||||
);
|
||||
).then((_) => controller.dispose());
|
||||
}
|
||||
|
||||
void updateMathEquation(String mathEquation, BuildContext context) {
|
||||
|
||||
-1
@@ -478,7 +478,6 @@ class _ToolbarItemListViewState extends State<_ToolbarItemListView> {
|
||||
void dispose() {
|
||||
widget.editorState.selectionNotifier
|
||||
.removeListener(_debounceUpdatePilotPosition);
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/hover.dart';
|
||||
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class DocumentFavoriteButton extends StatelessWidget {
|
||||
const DocumentFavoriteButton({
|
||||
super.key,
|
||||
required this.view,
|
||||
});
|
||||
|
||||
final ViewPB view;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<FavoriteBloc, FavoriteState>(
|
||||
builder: (context, state) {
|
||||
final isFavorite = state.views.any((v) => v.id == view.id);
|
||||
return _buildFavoriteButton(context, isFavorite);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFavoriteButton(BuildContext context, bool isFavorite) {
|
||||
return FlowyTooltip(
|
||||
message: isFavorite
|
||||
? LocaleKeys.button_removeFromFavorites.tr()
|
||||
: LocaleKeys.button_addToFavorites.tr(),
|
||||
child: FlowyHover(
|
||||
child: GestureDetector(
|
||||
onTap: () =>
|
||||
context.read<FavoriteBloc>().add(FavoriteEvent.toggle(view)),
|
||||
child: _buildFavoriteIcon(context, isFavorite),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFavoriteIcon(BuildContext context, bool isFavorite) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(6),
|
||||
child: FlowySvg(
|
||||
isFavorite ? FlowySvgs.favorite_s : FlowySvgs.unfavorite_s,
|
||||
size: const Size(18, 18),
|
||||
color: Theme.of(context).iconTheme.color,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/plugins/document/presentation/more/cubit/document_appearance_cubit.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||
import 'package:flowy_infra_ui/widget/spacing.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class FontSizeStepper extends StatelessWidget {
|
||||
const FontSizeStepper({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<DocumentAppearanceCubit, DocumentAppearance>(
|
||||
builder: (context, state) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10),
|
||||
child: Row(
|
||||
children: [
|
||||
const FlowyText('A', fontSize: 14),
|
||||
const HSpace(6),
|
||||
Expanded(
|
||||
child: SliderTheme(
|
||||
data: Theme.of(context).sliderTheme.copyWith(
|
||||
showValueIndicator: ShowValueIndicator.never,
|
||||
thumbShape: const RoundSliderThumbShape(
|
||||
enabledThumbRadius: 8,
|
||||
),
|
||||
overlayShape: const RoundSliderOverlayShape(
|
||||
overlayRadius: 16,
|
||||
),
|
||||
),
|
||||
child: Slider(
|
||||
value: state.fontSize,
|
||||
min: 10,
|
||||
max: 24,
|
||||
divisions: 8,
|
||||
onChanged: (fontSize) => context
|
||||
.read<DocumentAppearanceCubit>()
|
||||
.syncFontSize(fontSize),
|
||||
),
|
||||
),
|
||||
),
|
||||
const HSpace(6),
|
||||
const FlowyText('A', fontSize: 20),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
-92
@@ -1,92 +0,0 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/more/cubit/document_appearance_cubit.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/theme_extension.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
typedef _FontSizeSelection = (String, double, bool);
|
||||
|
||||
class FontSizeSwitcher extends StatefulWidget {
|
||||
const FontSizeSwitcher({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
State<FontSizeSwitcher> createState() => _FontSizeSwitcherState();
|
||||
}
|
||||
|
||||
class _FontSizeSwitcherState extends State<FontSizeSwitcher> {
|
||||
final List<_FontSizeSelection> _fontSizes = [
|
||||
(LocaleKeys.moreAction_small.tr(), 14.0, false),
|
||||
(LocaleKeys.moreAction_medium.tr(), 18.0, true),
|
||||
(LocaleKeys.moreAction_large.tr(), 22.0, false),
|
||||
];
|
||||
|
||||
_FontSizeSelection? _selection;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final selectedBgColor = AFThemeExtension.of(context).toggleButtonBGColor;
|
||||
final foregroundColor = Theme.of(context).colorScheme.onBackground;
|
||||
return BlocBuilder<DocumentAppearanceCubit, DocumentAppearance>(
|
||||
builder: (context, state) {
|
||||
_selection = _fontSizes.firstWhereOrNull(
|
||||
(element) => element.$2 == state.fontSize,
|
||||
);
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
FlowyText.semibold(
|
||||
LocaleKeys.moreAction_fontSize.tr(),
|
||||
fontSize: 12,
|
||||
color: Theme.of(context).colorScheme.tertiary,
|
||||
),
|
||||
const SizedBox(
|
||||
height: 5,
|
||||
),
|
||||
SegmentedButton<_FontSizeSelection>(
|
||||
showSelectedIcon: false,
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: foregroundColor,
|
||||
shadowColor: selectedBgColor.withOpacity(0.3),
|
||||
padding: const EdgeInsets.all(16),
|
||||
side: BorderSide(
|
||||
width: 0.5,
|
||||
color: foregroundColor,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
),
|
||||
),
|
||||
segments: _fontSizes
|
||||
.map(
|
||||
(e) => ButtonSegment(
|
||||
value: e,
|
||||
label: FlowyText(
|
||||
e.$1,
|
||||
fontSize: e.$2,
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
selected: {
|
||||
_selection ?? _fontSizes.first,
|
||||
},
|
||||
onSelectionChanged: (Set<(String, double, bool)> newSelection) {
|
||||
_selection = newSelection.firstOrNull;
|
||||
_updateSelectedFontSize(newSelection.first.$2);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _updateSelectedFontSize(double fontSize) {
|
||||
context.read<DocumentAppearanceCubit>().syncFontSize(fontSize);
|
||||
}
|
||||
}
|
||||
@@ -1,24 +1,55 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/more/cubit/document_appearance_cubit.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/more/font_size_switcher.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/more/font_size_slider.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/theme_extension.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/hover.dart';
|
||||
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class DocumentMoreButton extends StatelessWidget {
|
||||
const DocumentMoreButton({
|
||||
super.key,
|
||||
});
|
||||
const DocumentMoreButton({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AppFlowyPopover(
|
||||
constraints: const BoxConstraints(maxWidth: 400, maxHeight: 320),
|
||||
constraints: BoxConstraints.loose(const Size(200, 400)),
|
||||
offset: const Offset(0, 30),
|
||||
popupBuilder: (_) {
|
||||
final actions = [
|
||||
AppFlowyPopover(
|
||||
direction: PopoverDirection.leftWithCenterAligned,
|
||||
constraints: const BoxConstraints(maxHeight: 40, maxWidth: 240),
|
||||
offset: const Offset(-10, 0),
|
||||
popupBuilder: (context) => const FontSizeStepper(),
|
||||
child: FlowyButton(
|
||||
text: FlowyText.regular(
|
||||
LocaleKeys.moreAction_fontSize.tr(),
|
||||
color: AFThemeExtension.of(context).textColor,
|
||||
),
|
||||
leftIcon: Icon(
|
||||
Icons.format_size_sharp,
|
||||
color: Theme.of(context).iconTheme.color,
|
||||
size: 18,
|
||||
),
|
||||
leftIconSize: const Size(18, 18),
|
||||
hoverColor: AFThemeExtension.of(context).lightGreyHover,
|
||||
),
|
||||
),
|
||||
];
|
||||
|
||||
return ListView.separated(
|
||||
shrinkWrap: true,
|
||||
padding: EdgeInsets.zero,
|
||||
itemCount: actions.length,
|
||||
separatorBuilder: (_, __) => const VSpace(4),
|
||||
physics: StyledScrollPhysics(),
|
||||
itemBuilder: (_, index) => actions[index],
|
||||
);
|
||||
},
|
||||
child: FlowyTooltip(
|
||||
message: LocaleKeys.moreAction_moreOptions.tr(),
|
||||
child: FlowyHover(
|
||||
@@ -32,20 +63,6 @@ class DocumentMoreButton extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
popupBuilder: (context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
BlocProvider.value(
|
||||
value: context.read<DocumentAppearanceCubit>(),
|
||||
child: const FontSizeSwitcher(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
+7
@@ -134,6 +134,13 @@ class _InlineActionsHandlerState extends State<InlineActionsHandler> {
|
||||
startOffset = widget.editorState.selection?.endIndex ?? 0;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_scrollController.dispose();
|
||||
_focusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Focus(
|
||||
|
||||
Loaded 100 of 512 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user