Compare commits

..
37 Commits
Author SHA1 Message Date
Nathan.fooo e81a2ff577 fix: token refresh on local (#4650)
* fix: refresh user token on local

* chore: add test
2024-02-14 09:38:05 +08:00
Mathias Mogensen b356927e60 feat: improve more action in document (#4639) 2024-02-09 15:54:05 +01:00
Mathias Mogensen 1894409dc3 ci: resolve mobile build failing due to missing space (#4640)
* chore: remove additionally unused tools

* chore: test

* chore: bunch cleanup commands
2024-02-09 14:40:06 +01:00
nathan f29f1bae32 chore: update error message 2024-02-09 10:18:52 +08:00
Nathan.fooo 45054e7639 chore: bump version 047 (#4635) 2024-02-09 00:18:51 +08:00
Nathan.fooo 3b51a6e6be chore: reload workspace when fail to load at the first time (#4633)
* chore: reload workspace when fail to load at the first time

* fix: clippy

* chore: update client api

* chore: fix wasm build

* chore: fix test
2024-02-08 23:53:05 +08:00
Ansah Mohammad 747fe40648 feat: toggle favourites from the documents top bar. (#4382)
* feat: init star button

* feat: implemented star button on the right bar

* feat: Implemented toggle logic

* chore: Added ToolTip for favorite button

* refactor: renamed star to favorite

* refactor: moved appplication logic into favorite_cubit

* chore: arranged imports in alphabetical order

* refactor: reused favorite_bloc for toggle

* fix: fixed the icon no rebuild

* refactor: moved the FavoriteBloc higher in the widget tree

* refactor: moved FavoriteBloc inside multiBlocProvider of desktop_home_screen

* chore: specified bloc type

* chore: cleaned code

* refactor: use any instead of map
2024-02-08 10:45:54 +01:00
Kilu.He 60fc5bb2c8 fix: tauri folder bugs (#4589) 2024-02-08 14:22:44 +08:00
Mathias Mogensen 9d71464f1a fix: select name by default (#4630) 2024-02-07 21:49:17 +01:00
Mathias Mogensen 92ceb2cd7d fix: dispose of resources in time (#4625)
* fix: limit length when renaming views

* chore: clean up database listeners

* fix: dispose of controllers properly

* fix: dispose of resources properly

* fix: deleting filters with same name

* chore: extend DatabaseTabBarItemBuilder

* fix: null check on null value
2024-02-07 18:38:53 +01:00
Richard Shiue 0db21b91a3 fix: unequal grid header and body widths (#4626) 2024-02-07 17:49:53 +01:00
Richard Shiue 88e9d63a13 fix: show newChecklistTask on hover over entire list item in row detail page (#4622) 2024-02-07 23:20:26 +08:00
Mathias Mogensen 3129fa6cc1 fix: use cmd+. to toggle sidebar on macOS (#4621)
* fix: use cmd+. to hide sidebar on macOS

* fix: update missed tooltip
2024-02-07 12:18:13 +08:00
Mathias Mogensen db5372c18e feat: hotkey to open settings dialog (#4620)
* feat: hotkey to open settings dialog

* chore: change to cmd/ctrl+,
2024-02-07 12:17:48 +08:00
Richard Shiue b781c9aa0f fix: deleting a sorting field doesn't delete existing sorts (#4611) 2024-02-07 10:47:17 +08:00
Richard Shiue a9c6b80f4a chore: add spacing between field buttons in setting popover (#4612) 2024-02-07 10:46:36 +08:00
Lucas.Xu 271f781cce fix: application will raise an error when selecting unsplash image in local mode (#4604) 2024-02-05 21:53:14 +07:00
Richard Shiue ef4bce25d8 feat: sort by last modified time or created time (#4601) 2024-02-05 21:54:42 +08:00
ansizheshi 95c4cb241a chore: update zh-CN translations (#4599) 2024-02-05 17:39:45 +08:00
Dorian Péron e20d938ce3 chore: update translations (#4581) 2024-02-05 16:53:18 +08:00
Richard Shiue a515715543 feat: reorder sort precedence (#4592)
* feat: reorder sorts

* chore: add tests, fix tests, fix tauri build and fix clippy

* fix: add missing import
2024-02-05 13:52:59 +08:00
Nathan.fooo fda70ff560 feat: folder web (#4580)
* chore: folder wasm

* chore: folder wasm

* chore: resolve deps

* chore: fix trait

* chore: try localset

* chore: fix

* chore: fix

* chore: fix

* chore: async init sdk

* chore: fix test

* chore: fix test
2024-02-04 05:50:23 +08:00
Zackandnathan 08938b8c70 feat: cloud workspace api (#4469)
* feat: workspace api

* feat: added cloud apis for add and delete workspace

* feat: add and delete workspace event handlers

* chore: rust fmt

* chore: save user workspace

* test: add test

* test: add test

* chore: add to gitignore

* feat: update api add name to workspace

* chore: cargo clippy and rename to create

* chore: add envrc and direnv to gitignore

* chore: change name to create workspace instead of add workspace

* chore: update client api rev

* feat: add create workspace impl

* chore: restore gitignore to original

* test: fix create workspace event test

* fix: change delete workspace input

* fix: compile

* fix: create workspace test

* feat: add error code for request payload too large

* chore: remove cargo backup files

* feat: add is async option for upload file handler

* chore: update client api version

---------

Co-authored-by: nathan <nathan@appflowy.io>
2024-02-04 05:49:45 +08:00
Richard Shiueandnathan 250f29f325 chore: clean up sort and filter code (#4585)
* refactor: port away from extra SortType struct

* refactor: add validator to flowy_database and clean up unused structs

* refactor: port away from extra FilterType struct

* chore: analysis options

* fix: clippy and dart/ts compile

* fix: tauri build

---------

Co-authored-by: nathan <nathan@appflowy.io>
2024-02-04 05:49:28 +08:00
Yijing Huang d117cd6b5b chore: delete placeholder code and clean code (#4503) 2024-02-04 00:56:50 +08:00
Richard Shiue ca93cb20ec chore: revamp mobile database toolbar (#4579)
* chore: revamp mobile database toolbar

* chore: code cleanup
2024-02-04 00:55:44 +08:00
Richard Shiue 8c1d0106dd chore: inkwell mobile quick action sheets (#4576)
* chore: inkwell mobile quick action sheets

* chore: more subtle tap effect

* chore: improve code style
2024-02-04 00:55:05 +08:00
Mathias Mogensenandnathan 5cbc8b1e18 feat: calculations (#4473)
* feat: initial calculation controller

* fix: entities

* feat: calculations

* fix: review comments and support floats

* fix: abstract business logic into calculations service

* fix: clean calculation entities after merge

* feat: react to changes to row/cell/field_type

* chore: changes after merging main

* feat: handle delete field

* test: add grid calculations tests

* fix: add validation + format numbers

* refactor: get cell number

* chore: bump collab

* chore: fix clippy

* chore: update docs

* chore: update docs

* chore: fmt

* chore: fix flutter

* chore: collab rev

* fix: cleanup and hover to show

* fix: localization

* test: add basic rust test

* fix: clippy

* fix: support updating calculation on duplicate row

---------

Co-authored-by: nathan <nathan@appflowy.io>
2024-02-04 00:52:38 +08:00
Nathan.fooo 274742e334 chore: bump version v0.4.6 (#4595)
* chore: bump version

* chore: bump client api version
2024-02-03 19:45:46 +08:00
Lucas.Xu 792f56db79 chore: remove unused build and add build bot (#4586) 2024-02-02 17:42:23 +08:00
nathan 458d3f971b chore: bump client api 2024-02-02 09:35:55 +08:00
Nathan.fooo 2ffd6bf7ea chore: bump version (#4583) 2024-02-02 09:30:27 +08:00
Nathan.fooo 722691c922 chore: fix ws conn (#4582)
* chore: fix ws conn

* chore: flutter analyzer

* chore: update client api
2024-02-02 09:27:11 +08:00
Kilu.He 9746852b5f fix: fixed editor bugs (#4552)
* fix: fixed editor bugs

* fix: add error boundary
2024-02-02 05:47:41 +08:00
Lucas.Xu 5b030303a6 fix: appimage build (#4578)
* chore: force exec flutter package pub get

* feat: support building appimage on github ci
2024-02-01 20:58:58 +08:00
XiaoFans 1e4f342fc3 chore: update translations (#4577) 2024-02-01 20:53:49 +08:00
Richard Shiue 4f0379bdd7 fix: size of hitbox in app bar button (#4575) 2024-02-01 20:53:21 +08:00
512 changed files with 13200 additions and 6254 deletions

No files matched your search

-99
View File
@@ -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/
+29
View File
@@ -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 }}
+42
View File
@@ -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"
+6 -12
View File
@@ -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:
+20 -3
View File
@@ -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:
+12
View File
@@ -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.
+2 -1
View File
@@ -26,7 +26,7 @@ CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true
CARGO_MAKE_CRATE_FS_NAME = "dart_ffi"
CARGO_MAKE_CRATE_NAME = "dart-ffi"
LIB_NAME = "dart_ffi"
APPFLOWY_VERSION = "0.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
View File
@@ -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(
@@ -44,7 +44,6 @@ class _MobileBottomSheetEditLinkWidgetState
void dispose() {
textController.dispose();
hrefController.dispose();
super.dispose();
}
@@ -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();
}
@@ -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,
),
@@ -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,
),
@@ -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;
@@ -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;
@@ -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 {
@@ -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();
}
@@ -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],
);
@@ -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 {
@@ -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';
@@ -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
@@ -23,6 +23,7 @@ class _EditUsernameBottomSheetState extends State<EditUsernameBottomSheet> {
late TextEditingController _textFieldController;
final _formKey = GlobalKey<FormState>();
@override
void initState() {
super.initState();
@@ -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,
),
),
],
),
),
),
);
@@ -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',
),
};
}
@@ -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;
}
}
@@ -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();
}
}
@@ -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) {
@@ -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) {
@@ -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) {
@@ -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) {
@@ -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) {
@@ -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) {
@@ -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) {
@@ -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) {
@@ -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;
@@ -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;
}
}
@@ -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,
@@ -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
@@ -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,
@@ -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: {},
);
}
@@ -73,7 +73,7 @@ class GridFilterMenuBloc
}
@override
Future<void> close() {
Future<void> close() async {
if (_onFilterFn != null) {
fieldController.removeListener(onFiltersListener: _onFilterFn!);
_onFilterFn = null;
@@ -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',
@@ -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,
@@ -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;
}
}
@@ -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,
),
],
),
),
),
);
}
}
@@ -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();
},
),
);
}
}
@@ -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],
),
),
],
),
);
},
),
);
}
}
@@ -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();
},
),
);
}
}
@@ -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(),
);
@@ -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(),
);
}
@@ -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),
@@ -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();
}
}
@@ -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,
],
);
@@ -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),
);
},
),
);
@@ -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();
@@ -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(
@@ -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 =>
@@ -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() {
@@ -85,7 +85,7 @@ class GridChecklistCellState extends GridCellState<EditableChecklistCell> {
}
@override
void requestBeginFocus() {
void onRequestFocus() {
if (widget.skin is DesktopGridChecklistCellSkin) {
_popover.show();
}
@@ -84,7 +84,7 @@ class _DateCellState extends GridCellState<EditableDateCell> {
}
@override
void requestBeginFocus() {
void onRequestFocus() {
_popover.show();
widget.cellContainerNotifier.isFocus = true;
}
@@ -96,7 +96,7 @@ class _NumberCellState extends GridEditableTextCell<EditableNumberCell> {
SingleListenerFocusNode focusNode = SingleListenerFocusNode();
@override
void requestBeginFocus() {
void onRequestFocus() {
focusNode.requestFocus();
}
@@ -92,5 +92,5 @@ class _SelectOptionCellState extends GridCellState<EditableSelectOptionCell> {
}
@override
void requestBeginFocus() => _popover.show();
void onRequestFocus() => _popover.show();
}
@@ -97,7 +97,7 @@ class _TextCellState extends GridEditableTextCell<EditableTextCell> {
SingleListenerFocusNode focusNode = SingleListenerFocusNode();
@override
void requestBeginFocus() {
void onRequestFocus() {
focusNode.requestFocus();
}
@@ -83,7 +83,7 @@ class _TimestampCellState extends GridCellState<EditableTimestampCell> {
}
@override
void requestBeginFocus() {
void onRequestFocus() {
widget.cellContainerNotifier.isFocus = true;
}
@@ -89,6 +89,7 @@ class _GridURLCellState extends GridEditableTextCell<EditableURLCell> {
@override
void dispose() {
widget._cellDataNotifier.dispose();
_textEditingController.dispose();
cellBloc.close();
super.dispose();
@@ -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());
}
}
@@ -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(
@@ -37,6 +37,7 @@ class _SelectOptionCellEditorState extends State<SelectOptionCellEditor> {
@override
void dispose() {
popoverMutex.dispose();
textEditingController.dispose();
super.dispose();
}
@@ -162,7 +162,6 @@ class _SelectOptionTextFieldState extends State<SelectOptionTextField> {
},
),
child: SingleChildScrollView(
controller: ScrollController(),
scrollDirection: Axis.horizontal,
child: Wrap(spacing: 4, children: children),
),
@@ -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: [
@@ -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,
),
);
},
),
);
},
),
@@ -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(),
],
);
@@ -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(
@@ -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,
@@ -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
@@ -120,6 +120,12 @@ class _MathInputTextFieldState extends State<MathInputTextField> {
);
}
@override
void dispose() {
textEditingController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return SizedBox(
@@ -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) {
@@ -478,7 +478,6 @@ class _ToolbarItemListViewState extends State<_ToolbarItemListView> {
void dispose() {
widget.editorState.selectionNotifier
.removeListener(_debounceUpdatePilotPosition);
super.dispose();
}
@@ -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,
),
);
}
}
@@ -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),
],
),
);
},
);
}
}
@@ -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(),
),
],
),
);
},
);
}
}
@@ -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