Compare commits

..
41 Commits
Author SHA1 Message Date
Lucas.Xu 655a2b7093 chore: bump version 0.6.3 (#5701) 2024-07-08 13:46:27 +08:00
23c67bcdba feat: support publish document (#5576)
* feat: support a event for getting encoded collab of document

* feat: support publish view and unpublish views

* feat: publish page to the web

* chore: refacotor share bloc

* feat: call the publish event

* feat: support publish view and unpublish views

* feat: integrate publish api

* feat: integrate unpublish api

* feat: fetch the publish info to show the publish status

* feat: support publish interfaces

* fix: lint error

* fix: modified web server

* fix: some style

* fix: some style

* fix: some style

* fix: some style

* fix: some style

* fix: some style

* fix: some style

* fix: some style

* fix: some style

* fix: update codes

* fix: update codes

* fix: update codes

* fix: update codes

* fix: update codes

* chore: refactor publish bloc

* fix: some style

* fix: some style

* fix: some style

* fix: some style

* fix: some style

* fix: some style

* fix: the name is too long to publish

* chore: change color

* fix: some style

* fix: some style

* feat: refacotor share menu UI

* fix: some style

* fix: lint

* fix: some style

* feat: refacotor export-as

* fix: some style

* chore: refactor share menu colors

* fix: rust ci

* fix: some style

* fix: some style

* fix: deploy

* fix: deploy

* fix: deploy

* fix: deploy

* fix: deploy

* fix: deploy

* fix: deploy

* fix: deploy

* fix: deploy

* fix: deploy

* fix: deploy

* fix: deploy

* fix: bugs

* fix: bugs

* fix: bugs

* fix: bugs

* fix: bugs

* fix: bugs

* fix: bugs

* fix: bugs

* fix: bugs

* fix: bugs

* fix: bugs

* fix: bugs

* fix: bugs

* fix: bugs

* fix: bugs

* fix: bugs

* fix: rerelease

* fix: deploy

* fix: deploy

* fix: deploy

* fix: deploy

* fix: deploy

* fix: deploy

* fix: deploy

* fix: og image

* fix: support copy button

* fix: support copy button

* fix: support copy button

* chore: add a params

* feat: use default publish name

* chore: update copy

* feat: show a confirm deletion dialog if the deleted page contains published page

* feat: add copy toast in publish tab

* fix: to 404

fix: to 404

fix: to 404

fix: the error to 404

* feat: unpublish the page auto when moving it to another space

* feat: improve confirm deletion dialog

* feat: show unpublish error

* chore: use beta.appflowy.com

* feat: disable publish in non-apppflowy-cloud user mode

* fix: modified bullted icon style

* fix: the dark mode color

* fix: save the dark mode in local storage

* fix: text color

* chore: make bash script more portable (#5679)

* fix: title longer

* chore: move the files and modified the en

* chore: update deploy.sh

* chore: modified Dockerfile

* chore: modified server.cjs to server.js

* chore: modifed server.js to server.ts

* chore: replace publish url

* chore: remove todo list hover

* chore: show confirm dialog before deleting page

* fix: unpublish the pages before deleting

* fix: table cell bg color

* fix: callout icon

* fix: list number

* fix: emoji

* fix: number icon

* fix: callout icon position

* fix: add margin bottom

* fix: code block

* fix: support scroll for breadcrumbs

* fix: the breadcrumb doesn't update after moving page

* fix: 0705 issues

* fix: update publish status afer deleting page

* chore: add hover effect for visit site button

* fix: remove puiblish url text field enable border color

* chore: update delete page copy

* chore: enable debug category

* fix: only render sidebar if the spaces are ready

* fix: the breadcrumb doesn't update after moving page

* fix: auto code

* fix: add emoji

* fix: add emoji

* fix: favicon

* fix: cypress test

* fix: remove deploy ci

* fix: default url

* chore: revert launch.json

* fix: docker ci

* fix: change favicon

* fix: flutter integration test

* feat: add hover effect to share menu

* chore: add a checkmark if the page has been published

* chore: revert space deletion

---------

Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io>
Co-authored-by: Zack <speed2exe@live.com.sg>
2024-07-08 13:45:57 +08:00
Nathan.fooo 521fffd97c refactor: extract chat plugin to new repo (#5699)
* chore: update ui

* chore: update

* chore: update

* chore: separate to new crate

* chore: update commit id

* chore: fix compile

* chore: bump version
2024-07-08 13:19:13 +08:00
Lucas.Xu 2ecc2a67a9 fix: take the max value of the keyboard height and the view insets bottom to make the toolbar visible (#5700)
* fix: android toolbar will be blocked if the scroll bar is hidden

* fix: last opened view is synced in same workspace

* fix: fallback to space if the section sidebar contains space

* chore: revert last opened view code

* fix: integration tests
2024-07-08 13:15:42 +08:00
Lucas.Xu 2d11215bb2 chore: use latest macos runner to build the release package (#5686) 2024-07-08 09:43:23 +08:00
Lucas.Xu 6d0c9f766b fix: filter chat page when duplicating (#5676)
* fix: filter chat page when duplicating

* fix: don't clear selection if it has been disposed
2024-07-02 14:35:29 +08:00
Nathan.fooo 7a3f013a87 chore: show ai service error (#5675) 2024-07-02 13:26:53 +08:00
Lucas.Xu 8c1520b273 feat: sync the created view after duplicating (#5674)
* feat: sync the created view after duplicating

* chore: revert launch.json

* chore: refacotor code
2024-07-02 13:02:15 +08:00
Mathias MogensenandLucas.Xu a7b850e752 chore: disable cloud search (#5663)
Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io>
2024-07-02 13:02:01 +08:00
Lucas.Xu 42dc6017c6 feat: support moving page to a space (#5665)
* feat: support moving page to a space

* chore: update collab repo
2024-07-02 10:29:46 +08:00
Stefan Weiberg f9fba6b561 chore: update German translations (#5640) 2024-07-02 09:44:16 +08:00
Nathan.fooo 46896b5a2b chore: remove appflowy_web project (#5671)
* chore: remove appflowy_web project

* chore: remove ci
2024-07-01 22:49:24 +08:00
Lucas.Xu 2b8dca209e feat: sync the documents and databases after batch importing documents and databases (#5644)
* feat: support batch import

* feat: support batch import database

* chore: revert launch.json

* chore: fix rust ci

* fix: rust ci
2024-07-01 14:44:08 +08:00
Lucas.Xu c78f23e1c0 fix: v0.6.2 issues (#5654)
* fix: remove create button in move to menu

* fix: add loading indicator when duplicating space

* fix: sidebar header expand icon status

* fix: text within select tag overflow

* fix: callout block icon align issue

* feat: sync the space icon when creating a space

* fix: duplicated hover views

* fix: cover image doesn't update
2024-07-01 14:43:57 +08:00
Lucas.Xu 50f5be3e75 chore: fix rustfmt ci test (#5664) 2024-07-01 13:26:43 +08:00
Nathan.fooo e1c68c1b72 feat: Run Local AI model in AppFlowy (#5655)
* chore: load plugin

* chore: sidecar

* chore: fix test

* chore: clippy

* chore: save chat config

* chore: arc plugin

* chore: add plugins

* chore: clippy

* chore: test streaming

* chore: config chat

* chore: stream message

* chore: response with local ai

* chore: fix compile

* chore: config ui

* chore: fix load plugin

* chore: add docs

* chore: update docs

* chore: disable local ai

* chore: fix compile

* chore: clippy
2024-06-30 17:38:39 +08:00
Mathias Mogensen 3bcadff152 fix: settings changes + ai layout improvements (#5656)
* fix: settings changes

* fix: improve chat layout
2024-06-28 22:54:54 +08:00
Mathias Mogensen b09ff040f1 fix: postpone fetching member (#5648) 2024-06-28 09:03:30 +08:00
Nathan.fooo bf087bba5d fix: score filter (#5645)
* fix: score filter

* chore: update score limit
2024-06-28 07:57:12 +08:00
Lucas.Xu 605a53f2ae feat: open the duplicated space by default (#5638) 2024-06-27 14:12:52 +08:00
Stefan Weiberg ecc5555a4e chore: update German translations (#5630) 2024-06-27 13:34:51 +08:00
Lucas.Xu d2a5bab6d9 chore: bump version 0.6.2 (#5637) 2024-06-27 12:02:15 +08:00
Lucas.Xu a1dec0f269 feat: add check workspace health button in settings page (#5631) 2024-06-27 09:57:06 +08:00
Mohammad Zolfaghari b24fbc6b60 fix: fill field_settings on creating linked view (#5598) 2024-06-27 03:07:08 +02:00
Mathias Mogensen bbb3f95a13 fix: billing+cleanup (#5634)
* fix: billing fixes

* chore: best practice and cleaning

* chore: dispose of unused resources

* chore: upgrade editor version
2024-06-27 02:00:21 +02:00
Mathias Mogensen 7859fc7922 fix: macos fullscreen window position (#5632) 2024-06-27 00:36:59 +02:00
Lucas.Xu f812040f04 feat: support creating database at the first level (#5627)
* feat: support creating database at the first level

* chore: add loading indicator when importing file
2024-06-26 13:27:33 +08:00
Lucas.Xu 7d96c2dfd4 feat: add key + as zoom-in key (#5628) 2024-06-26 13:27:13 +08:00
Mathias Mogensen e4132ed217 chore: enable cloud search (#5624)
* chore: enable cloud search

* fix: filter received msgs by query
2024-06-25 21:56:58 +02:00
Mathias Mogensen a7f40b2adc fix: paste in list (#5621)
* fix: support pasting in list

* test: add simple test

* chore: remove debugPrint
2024-06-25 20:08:01 +02:00
Mathias Mogensen c996c9c28e fix: maximized window optimization (#5622) 2024-06-25 20:07:43 +02:00
MarcinandLucas.Xu f59b8a2e5c chore: update Polish translations (#5593)
* chore: update translations with Fink 🐦

* Revert "chore: update translations with Fink 🐦"

This reverts commit e3b7ca9e22.

* chore: update pl-PL translations

---------

Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io>
2024-06-25 21:16:35 +08:00
JakobandLucas.Xu f7480cba16 chore: update de-DE translations (#5588)
* chore: update translations with Fink 🐦

* Revert "chore: update translations with Fink 🐦"

This reverts commit f24a479b63.

* chore: update de-DE translations

---------

Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io>
2024-06-25 21:09:02 +08:00
Mathias Mogensen 271b250eac fix: make theme uploads more tolerant (#5617) 2024-06-25 13:52:59 +02:00
Nathan.foooandLucas.Xu 6b1e7b6ac8 chore: set document id (#5585)
* chore: set document id

* feat: set document id to upload image api

* chore: fmt

---------

Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io>
2024-06-25 17:36:41 +08:00
Lucas.Xu bf2a00b133 fix: prevent overfow of space name and view name (#5619)
* fix: space name overflow

* fix: space name overflow in space menu

* fix: view name overflow in favorite menu
2024-06-25 17:36:00 +08:00
Lucas.Xu a8ed93054c feat: support moving page across spaces (#5618)
* feat: support moving page across spaces

* feat: refacotor move api

* feat: filter the database views

* feat: support searching in move page menu
2024-06-25 16:43:58 +08:00
Lucas.Xu b9ad2768cf feat: duplicate space (#5612) 2024-06-25 10:03:02 +08:00
54c9d12171 feat: support switch model (#5575)
* feat: ai settings page

* chore: intergate client api

* chore: replace open ai calls

* chore: disable gen image from ai

* chore: clippy

* chore: remove learn about ai

* chore: fix wanrings

* chore: fix restart button title

* chore: remove await

* chore: remove loading indicator

---------

Co-authored-by: nathan <nathan@appflowy.io>
Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io>
2024-06-25 07:59:38 +08:00
Lucas.Xu 40312f4260 fix: infinite loading (#5613) 2024-06-24 20:14:22 +08:00
Zack d2ca41c8f5 Remove deprecated add member (#5611)
* chore: remove deprecated add member directly

* chore: cargo clippy

* chore: cargo clippy

* chore: cargo clippy
2024-06-24 14:19:36 +08:00
527 changed files with 9928 additions and 15012 deletions

No files matched your search

-72
View File
@@ -1,72 +0,0 @@
name: Deploy Web (Test)
on:
push:
branches:
- build/test
env:
NODE_VERSION: "18.16.0"
PNPM_VERSION: "8.5.0"
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
deploy:
runs-on: ubuntu-latest
env:
SSH_PRIVATE_KEY: ${{ secrets.WEB_TEST_SSH_PRIVATE_KEY }}
REMOTE_HOST: ${{ secrets.WEB_TEST_REMOTE_HOST }}
REMOTE_USER: ${{ secrets.WEB_TEST_REMOTE_USER }}
SSL_CERTIFICATE: ${{ secrets.WEB_TEST_SSL_CERTIFICATE }}
SSL_CERTIFICATE_KEY: ${{ secrets.WEB_TEST_SSL_CERTIFICATE_KEY }}
ENV_FILE: test.env
steps:
- uses: actions/checkout@v4
- name: setup node
uses: actions/setup-node@v3
with:
node-version: ${{ env.NODE_VERSION }}
- name: setup pnpm
uses: pnpm/action-setup@v2
with:
version: ${{ env.PNPM_VERSION }}
- name: Node_modules cache
uses: actions/cache@v2
with:
path: frontend/appflowy_web_app/node_modules
key: node-modules-${{ runner.os }}
- name: install frontend dependencies
working-directory: frontend/appflowy_web_app
run: |
pnpm install
- name: copy env file
working-directory: frontend/appflowy_web_app
run: |
cp ${{ env.ENV_FILE }} .env
- name: test and lint
working-directory: frontend/appflowy_web_app
run: |
pnpm run lint
- name: build
working-directory: frontend/appflowy_web_app
run: |
pnpm run build
- name: generate SSL certificate
run: |
echo "${{ env.SSL_CERTIFICATE }}" > nginx-signed.crt
echo "${{ env.SSL_CERTIFICATE_KEY }}" > nginx-signed.key
- name: Deploy to EC2
uses: easingthemes/ssh-deploy@main
with:
SSH_PRIVATE_KEY: ${{ env.SSH_PRIVATE_KEY }}
ARGS: "-rlgoDzvc -i"
SOURCE: "frontend/appflowy_web_app/dist frontend/appflowy_web_app/server.cjs frontend/appflowy_web_app/start.sh frontend/appflowy_web_app/Dockerfile frontend/appflowy_web_app/nginx.conf frontend/appflowy_web_app/.env nginx-signed.crt nginx-signed.key"
REMOTE_HOST: ${{ env.REMOTE_HOST }}
REMOTE_USER: ${{ env.REMOTE_USER }}
EXCLUDE: "frontend/appflowy_web_app/dist/, frontend/appflowy_web_app/node_modules/"
SCRIPT_AFTER: |
docker build -t appflowy-web-app .
docker rm -f appflowy-web-app || true
docker run -d -p 80:80 -p 443:443 --env-file .env --name appflowy-web-app appflowy-web-app
+2 -2
View File
@@ -135,7 +135,7 @@ jobs:
fail-fast: false
matrix:
job:
- { target: x86_64-apple-darwin, os: macos-11, extra-build-args: "" }
- { target: x86_64-apple-darwin, os: macos-latest, extra-build-args: "" }
steps:
- name: Checkout source code
uses: actions/checkout@v4
@@ -233,7 +233,7 @@ jobs:
job:
- {
targets: "aarch64-apple-darwin,x86_64-apple-darwin",
os: macos-11,
os: macos-latest,
extra-build-args: "",
}
steps:
-83
View File
@@ -1,83 +0,0 @@
name: WEB-CI
on:
workflow_dispatch:
inputs:
build:
description: 'Build the web app'
required: true
default: 'true'
env:
CARGO_TERM_COLOR: always
NODE_VERSION: "18.16.0"
PNPM_VERSION: "8.5.0"
RUST_TOOLCHAIN: "1.77.2"
CARGO_MAKE_VERSION: "0.36.6"
jobs:
web-build:
if: github.event.pull_request.draft != true
strategy:
fail-fast: false
matrix:
platform: [ ubuntu-latest ]
runs-on: ${{ matrix.platform }}
steps:
- uses: actions/checkout@v4
- name: setup node
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
- name: Cache Rust Dependencies
uses: Swatinem/rust-cache@v2
with:
key: rust-dependencies-${{ runner.os }}
workspaces: |
frontend/rust-lib
frontend/appflowy_web/appflowy_wasm
# TODO: Can combine caching deps and node_modules in one
# See Glob patterns: https://github.com/actions/toolkit/tree/main/packages/glob
- name: Cache Node.js dependencies
uses: actions/cache@v4
with:
path: ~/.npm
key: npm-${{ runner.os }}
- name: Cache node_modules
uses: actions/cache@v4
with:
path: frontend/appflowy_web/node_modules
key: node-modules-${{ runner.os }}
- name: Install Rust toolchain
id: rust_toolchain
uses: actions-rs/toolchain@v1
with:
toolchain: ${{ env.RUST_TOOLCHAIN }}
override: true
profile: minimal
- name: Install wasm-pack
run: cargo install wasm-pack
- uses: taiki-e/install-action@v2
with:
tool: cargo-make@${{ env.CARGO_MAKE_VERSION }}
- name: install dependencies
if: matrix.platform == 'ubuntu-latest'
working-directory: frontend
run: |
sudo apt-get update
npm install -g pnpm@${{ env.PNPM_VERSION }}
cargo make install_web_protobuf
- name: Build
working-directory: frontend/appflowy_web
run: |
pnpm install
pnpm run build_release_wasm
+21
View File
@@ -1,4 +1,25 @@
# Release Notes
## Version 0.6.3 - 08/07/2024
### New Features
- Publish a Document to the Web
## Version 0.6.2 - 01/07/2024
### New Features
- Added support for duplicating spaces.
- Added support for moving pages across spaces.
- Undo markdown formatting with `Ctrl + Z` or `Cmd + Z`.
- Improved shortcuts settings UI.
### Bug Fixes
- Fixed unable to zoom in with `Ctrl` and `+` or `Cmd` and `+` on some keyboards.
- Fixed unable to paste nested lists in existing lists.
## Version 0.6.1 - 22/06/2024
### New Features
- Introduced the "Space" feature to help you organize your pages more efficiently.
### Bug Fixes
- Resolved shortcut conflicts on the board page.
- Resolved an issue where underscores could cause the editor to freeze.
## Version 0.6.0 - 19/06/2024
### New Features
- Introduced the "Space" feature to help you organize your pages more efficiently.
+1 -2
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.6.1"
APPFLOWY_VERSION = "0.6.3"
FLUTTER_DESKTOP_FEATURES = "dart"
PRODUCT_NAME = "AppFlowy"
MACOSX_DEPLOYMENT_TARGET = "11.0"
@@ -50,7 +50,6 @@ 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"
TAURI_APP_BACKEND_SERVICE_PATH = "appflowy_web_app/src/application/services/tauri-services/backend"
# Test default config
TEST_CRATE_TYPE = "cdylib"
@@ -46,7 +46,6 @@ void main() {
await tester.ime.insertText(inputContent);
expect(find.text(inputContent, findRichText: true), findsOneWidget);
// TODO(nathan): remove the await
// 6 seconds for data sync
await tester.waitForSeconds(6);
@@ -1,10 +1,11 @@
import 'dart:io';
import 'package:flutter/services.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
@@ -116,6 +117,53 @@ void main() {
]);
});
});
testWidgets('paste bulleted list in numbered list', (tester) async {
const inAppJson =
'{"document":{"type":"page","children":[{"type":"bulleted_list","children":[{"type":"bulleted_list","data":{"delta":[{"insert":"World"}]}}],"data":{"delta":[{"insert":"Hello"}]}}]}}';
await tester.pasteContent(
inAppJson: inAppJson,
beforeTest: (editorState) async {
final transaction = editorState.transaction;
// Insert two numbered list nodes
// 1. Parent One
// 2.
transaction.insertNodes(
[0],
[
Node(
type: NumberedListBlockKeys.type,
attributes: {
'delta': [
{"insert": "One"},
],
},
),
Node(
type: NumberedListBlockKeys.type,
attributes: {'delta': []},
),
],
);
// Set the selection to the second numbered list node (which has empty delta)
transaction.afterSelection = Selection.collapsed(Position(path: [1]));
await editorState.apply(transaction);
await tester.pumpAndSettle();
},
(editorState) {
final secondNode = editorState.getNodeAtPath([1]);
expect(secondNode?.delta?.toPlainText(), 'Hello');
expect(secondNode?.children.length, 1);
final childNode = secondNode?.children.first;
expect(childNode?.delta?.toPlainText(), 'World');
expect(childNode?.type, BulletedListBlockKeys.type);
},
);
});
});
testWidgets('paste image(png) from memory', (tester) async {
@@ -210,42 +258,33 @@ void main() {
testWidgets('paste the html content contains section', (tester) async {
const html =
'''<meta charset='utf-8'><section style="margin: 0px 8px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgba(255, 255, 255, 0.6); font-family: system-ui, -apple-system, &quot;system-ui&quot;, &quot;Helvetica Neue&quot;, &quot;PingFang SC&quot;, &quot;Hiragino Sans GB&quot;, &quot;Microsoft YaHei UI&quot;, &quot;Microsoft YaHei&quot;, Arial, sans-serif; font-size: 15px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: 0.544px; orphans: 2; text-align: justify; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(25, 25, 25); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><span style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgb(0, 160, 113);"><strong style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important;">AppFlowy</strong></span></section><section style="margin: 0px 8px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgba(255, 255, 255, 0.6); font-family: system-ui, -apple-system, &quot;system-ui&quot;, &quot;Helvetica Neue&quot;, &quot;PingFang SC&quot;, &quot;Hiragino Sans GB&quot;, &quot;Microsoft YaHei UI&quot;, &quot;Microsoft YaHei&quot;, Arial, sans-serif; font-size: 15px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: 0.544px; orphans: 2; text-align: justify; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(25, 25, 25); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><span style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgb(0, 160, 113);"><strong style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important;">Hello World</strong></span></section>''';
await tester.pasteContent(
html: html,
(editorState) {
expect(editorState.document.root.children.length, 2);
final node1 = editorState.getNodeAtPath([0])!;
final node2 = editorState.getNodeAtPath([1])!;
expect(node1.type, ParagraphBlockKeys.type);
expect(node2.type, ParagraphBlockKeys.type);
},
);
await tester.pasteContent(html: html, (editorState) {
expect(editorState.document.root.children.length, 2);
final node1 = editorState.getNodeAtPath([0])!;
final node2 = editorState.getNodeAtPath([1])!;
expect(node1.type, ParagraphBlockKeys.type);
expect(node2.type, ParagraphBlockKeys.type);
});
});
testWidgets('paste the html from google translation', (tester) async {
const html =
'''<meta charset='utf-8'><section style="margin: 0px 8px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgba(255, 255, 255, 0.6); font-family: system-ui, -apple-system, &quot;system-ui&quot;, &quot;Helvetica Neue&quot;, &quot;PingFang SC&quot;, &quot;Hiragino Sans GB&quot;, &quot;Microsoft YaHei UI&quot;, &quot;Microsoft YaHei&quot;, Arial, sans-serif; font-size: 15px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: 0.544px; orphans: 2; text-align: justify; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(25, 25, 25); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><span style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgb(0, 160, 113);"><strong style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important;"><font style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; vertical-align: inherit;"><font style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; vertical-align: inherit;">new force</font></font></strong></span></section><section style="margin: 0px 8px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgba(255, 255, 255, 0.6); font-family: system-ui, -apple-system, &quot;system-ui&quot;, &quot;Helvetica Neue&quot;, &quot;PingFang SC&quot;, &quot;Hiragino Sans GB&quot;, &quot;Microsoft YaHei UI&quot;, &quot;Microsoft YaHei&quot;, Arial, sans-serif; font-size: 15px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: 0.544px; orphans: 2; text-align: justify; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(25, 25, 25); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><span style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgb(0, 160, 113);"><strong style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important;"><font style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; vertical-align: inherit;"><font style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; vertical-align: inherit;">Assessment focus: potential motivations, empathy</font></font></strong></span></section><section style="margin: 0px 8px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgba(255, 255, 255, 0.6); font-family: system-ui, -apple-system, &quot;system-ui&quot;, &quot;Helvetica Neue&quot;, &quot;PingFang SC&quot;, &quot;Hiragino Sans GB&quot;, &quot;Microsoft YaHei UI&quot;, &quot;Microsoft YaHei&quot;, Arial, sans-serif; font-size: 15px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: 0.544px; orphans: 2; text-align: justify; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(25, 25, 25); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><br style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important;"></section><section style="margin: 0px 8px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgba(255, 255, 255, 0.6); font-family: system-ui, -apple-system, &quot;system-ui&quot;, &quot;Helvetica Neue&quot;, &quot;PingFang SC&quot;, &quot;Hiragino Sans GB&quot;, &quot;Microsoft YaHei UI&quot;, &quot;Microsoft YaHei&quot;, Arial, sans-serif; font-size: 15px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: 0.544px; orphans: 2; text-align: justify; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(25, 25, 25); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><font style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-Line truncated
await tester.pasteContent(
html: html,
(editorState) {
expect(editorState.document.root.children.length, 8);
},
);
await tester.pasteContent(html: html, (editorState) {
expect(editorState.document.root.children.length, 8);
});
});
testWidgets(
'auto convert url to link preview block',
(widgetTester) async {
(tester) async {
const url = 'https://appflowy.io';
await widgetTester.pasteContent(
plainText: url,
(editorState) {
expect(editorState.document.root.children.length, 2);
final node = editorState.getNodeAtPath([0])!;
expect(node.type, LinkPreviewBlockKeys.type);
expect(node.attributes[LinkPreviewBlockKeys.url], url);
},
);
await tester.pasteContent(plainText: url, (editorState) {
expect(editorState.document.root.children.length, 2);
final node = editorState.getNodeAtPath([0])!;
expect(node.type, LinkPreviewBlockKeys.type);
expect(node.attributes[LinkPreviewBlockKeys.url], url);
});
},
);
}
@@ -256,6 +295,7 @@ extension on WidgetTester {
Future<void> Function(EditorState editorState)? beforeTest,
String? plainText,
String? html,
String? inAppJson,
(String, Uint8List?)? image,
}) async {
await initializeAppFlowy();
@@ -271,6 +311,7 @@ extension on WidgetTester {
ClipboardServiceData(
plainText: plainText,
html: html,
inAppJson: inAppJson,
image: image,
),
);
@@ -8,9 +8,10 @@ void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('Empty', () {
testWidgets('toggle theme mode', (tester) async {
testWidgets('empty test', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.wait(1000);
});
});
}
@@ -1,4 +1,4 @@
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/openai_client.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/ai_client.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_editor/src/render/toolbar/toolbar_widget.dart';
@@ -103,8 +103,8 @@ Future<AppFlowyEditor> setUpOpenAITesting(WidgetTester tester) async {
}
Future<void> mockOpenAIRepository() async {
await getIt.unregister<OpenAIRepository>();
getIt.registerFactoryAsync<OpenAIRepository>(
await getIt.unregister<AIRepository>();
getIt.registerFactoryAsync<AIRepository>(
() => Future.value(
MockOpenAIRepository(),
),
@@ -51,13 +51,14 @@ void main() {
},
);
final shareButton = find.byType(ShareActionList);
final shareButton = find.byType(DocumentShareButton);
final shareButtonState =
tester.state(shareButton) as ShareActionListState;
tester.widget(shareButton) as DocumentShareButton;
final path = await mockSaveFilePath(
p.join(
context.applicationDataDirectory,
'${shareButtonState.name}.md',
'${shareButtonState.view.name}.md',
),
);
@@ -44,7 +44,7 @@ class MockOpenAIRepository extends HttpOpenAIRepository {
required Future<void> Function() onStart,
required Future<void> Function(TextCompletionResponse response) onProcess,
required Future<void> Function() onEnd,
required void Function(OpenAIError error) onError,
required void Function(AIError error) onError,
String? suffix,
int maxTokens = 2048,
double temperature = 0.3,
+2 -2
View File
@@ -170,7 +170,7 @@ SPEC CHECKSUMS:
file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655
flowy_infra_ui: 0455e1fa8c51885aa1437848e361e99419f34ebc
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
fluttertoast: 31b00dabfa7fb7bacd9e7dbee580d7a2ff4bf265
fluttertoast: fafc4fa4d01a6a9e4f772ecd190ffa525e9e2d9c
image_gallery_saver: cb43cc43141711190510e92c460eb1655cd343cb
image_picker_ios: 99dfe1854b4fa34d0364e74a78448a0151025425
integration_test: ce0a3ffa1de96d1a89ca0ac26fca7ea18a749ef4
@@ -191,4 +191,4 @@ SPEC CHECKSUMS:
PODFILE CHECKSUM: d0d9b4ff572d8695c38eb3f9b490f55cdfc57eca
COCOAPODS: 1.15.2
COCOAPODS: 1.11.3
@@ -18,6 +18,13 @@ class KVKeys {
/// {'dx': 10.0, 'dy': 10.0}
static const String windowPosition = 'windowPosition';
/// The key for saving the window status
///
/// The value is a json string with the following format:
/// { 'windowMaximized': true }
///
static const String windowMaximized = 'windowMaximized';
static const String kDocumentAppearanceFontSize =
'kDocumentAppearanceFontSize';
static const String kDocumentAppearanceFontFamily =
@@ -54,6 +54,7 @@ class _MobileSpaceState extends State<MobileSpace> {
context.read<SpaceBloc>().add(
SpaceEvent.createPage(
name: LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
layout: ViewLayoutPB.Document,
index: 0,
),
);
@@ -99,6 +100,7 @@ class _MobileSpaceState extends State<MobileSpace> {
context.read<SpaceBloc>().add(
SpaceEvent.createPage(
name: LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
layout: ViewLayoutPB.Document,
),
);
}
@@ -119,7 +119,6 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
final uniqueMessages = {...allMessages, ...messages}.toList()
..sort((a, b) => b.id.compareTo(a.id));
uniqueMessages.insertAll(0, onetimeMessages);
emit(
state.copyWith(
messages: uniqueMessages,
@@ -380,7 +379,8 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
}
Message _createStreamMessage(AnswerStream stream, Int64 questionMessageId) {
final streamMessageId = nanoid();
final streamMessageId = (questionMessageId + 1).toString();
lastStreamMessageId = streamMessageId;
return TextMessage(
@@ -1,3 +1,5 @@
import 'package:flutter/material.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/plugins/ai_chat/chat_page.dart';
import 'package:appflowy/plugins/util.dart';
@@ -7,7 +9,6 @@ import 'package:appflowy/workspace/presentation/home/home_stack.dart';
import 'package:appflowy/workspace/presentation/widgets/tab_bar_item.dart';
import 'package:appflowy/workspace/presentation/widgets/view_title_bar.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class AIChatPluginBuilder extends PluginBuilder {
@@ -65,6 +66,12 @@ class AIChatPagePlugin extends Plugin {
_viewInfoBloc = ViewInfoBloc(view: notifier.view)
..add(const ViewInfoEvent.started());
}
@override
void dispose() {
_viewInfoBloc.close();
notifier.dispose();
}
}
class AIChatPagePluginWidgetBuilder extends PluginWidgetBuilder
@@ -111,4 +118,7 @@ class AIChatPagePluginWidgetBuilder extends PluginWidgetBuilder
@override
List<NavigationItem> get navigationItems => [this];
@override
EdgeInsets get contentPadding => EdgeInsets.zero;
}
@@ -1,3 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/ai_chat/application/chat_bloc.dart';
import 'package:appflowy/plugins/ai_chat/presentation/ai_message_bubble.dart';
@@ -10,12 +13,10 @@ 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:flowy_infra_ui/widget/spacing.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_chat_types/flutter_chat_types.dart';
import 'package:flutter_chat_ui/flutter_chat_ui.dart' show Chat;
import 'package:flutter_chat_types/flutter_chat_types.dart' as types;
import 'package:flutter_chat_ui/flutter_chat_ui.dart' show Chat;
import 'presentation/chat_input.dart';
import 'presentation/chat_popmenu.dart';
@@ -27,11 +28,11 @@ import 'presentation/message/user_text_message.dart';
class AIChatUILayout {
static EdgeInsets get chatPadding =>
isMobile ? EdgeInsets.zero : const EdgeInsets.symmetric(horizontal: 70);
isMobile ? EdgeInsets.zero : const EdgeInsets.symmetric(horizontal: 20);
static EdgeInsets get welcomePagePadding => isMobile
? const EdgeInsets.symmetric(horizontal: 20)
: const EdgeInsets.symmetric(horizontal: 100);
: const EdgeInsets.symmetric(horizontal: 50);
static double get messageWidthRatio => 0.85;
@@ -44,7 +45,8 @@ class AIChatUILayout {
query.padding.right,
query.viewInsets.bottom + query.padding.bottom,
)
: const EdgeInsets.symmetric(horizontal: 70);
: const EdgeInsets.symmetric(horizontal: 50) +
const EdgeInsets.only(bottom: 20);
}
}
@@ -77,92 +79,89 @@ class _AIChatPageState extends State<AIChatPage> {
Widget build(BuildContext context) {
if (widget.userProfile.authenticator == AuthenticatorPB.AppFlowyCloud) {
return buildChatWidget();
} else {
return Center(
child: FlowyText(
LocaleKeys.chat_unsupportedCloudPrompt.tr(),
fontSize: 20,
),
);
}
return Center(
child: FlowyText(
LocaleKeys.chat_unsupportedCloudPrompt.tr(),
fontSize: 20,
),
);
}
Widget buildChatWidget() {
return SizedBox.expand(
child: Padding(
padding: AIChatUILayout.chatPadding,
child: BlocProvider(
create: (context) => ChatBloc(
view: widget.view,
userProfile: widget.userProfile,
)..add(const ChatEvent.initialLoad()),
child: BlocBuilder<ChatBloc, ChatState>(
builder: (blocContext, state) {
return Chat(
messages: state.messages,
onAttachmentPressed: () {},
onSendPressed: (types.PartialText message) {
// We use custom bottom widget for chat input, so
// do not need to handle this event.
},
customBottomWidget: buildChatInput(blocContext),
user: _user,
theme: buildTheme(context),
onEndReached: () async {
if (state.hasMorePrevMessage &&
state.loadingPreviousStatus !=
const LoadingState.loading()) {
blocContext
.read<ChatBloc>()
.add(const ChatEvent.startLoadingPrevMessage());
}
},
emptyState: BlocBuilder<ChatBloc, ChatState>(
builder: (context, state) {
return state.initialLoadingStatus ==
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Flexible(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 784),
child: BlocProvider(
create: (_) => ChatBloc(
view: widget.view,
userProfile: widget.userProfile,
)..add(const ChatEvent.initialLoad()),
child: BlocBuilder<ChatBloc, ChatState>(
builder: (blocContext, state) => Chat(
messages: state.messages,
onSendPressed: (_) {
// We use custom bottom widget for chat input, so
// do not need to handle this event.
},
customBottomWidget: buildChatInput(blocContext),
user: _user,
theme: buildTheme(context),
onEndReached: () async {
if (state.hasMorePrevMessage &&
state.loadingPreviousStatus !=
const LoadingState.loading()) {
blocContext
.read<ChatBloc>()
.add(const ChatEvent.startLoadingPrevMessage());
}
},
emptyState: BlocBuilder<ChatBloc, ChatState>(
builder: (_, state) => state.initialLoadingStatus ==
const LoadingState.finish()
? Padding(
padding: AIChatUILayout.welcomePagePadding,
child: ChatWelcomePage(
onSelectedQuestion: (question) {
blocContext
.read<ChatBloc>()
.add(ChatEvent.sendMessage(question));
},
onSelectedQuestion: (question) => blocContext
.read<ChatBloc>()
.add(ChatEvent.sendMessage(question)),
),
)
: const Center(
child: CircularProgressIndicator.adaptive(),
);
),
),
messageWidthRatio: AIChatUILayout.messageWidthRatio,
textMessageBuilder: (
textMessage, {
required messageWidth,
required showName,
}) =>
_buildAITextMessage(blocContext, textMessage),
bubbleBuilder: (
child, {
required message,
required nextMessageInGroup,
}) {
if (message.author.id == _user.id) {
return ChatUserMessageBubble(
message: message,
child: child,
);
}
return _buildAIBubble(message, blocContext, state, child);
},
),
messageWidthRatio: AIChatUILayout.messageWidthRatio,
textMessageBuilder: (
textMessage, {
required messageWidth,
required showName,
}) {
return _buildAITextMessage(blocContext, textMessage);
},
bubbleBuilder: (
child, {
required message,
required nextMessageInGroup,
}) {
if (message.author.id == _user.id) {
return ChatUserMessageBubble(
message: message,
child: child,
);
} else {
return _buildAIBubble(message, blocContext, state, child);
}
},
);
},
),
),
),
),
),
],
);
}
@@ -1,9 +1,10 @@
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_ui/style_widget/hover.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flutter/material.dart';
import 'chat_input.dart';
@@ -31,19 +32,15 @@ class ChatWelcomePage extends StatelessWidget {
size: Size.square(44),
),
const SizedBox(height: 40),
GridView.builder(
shrinkWrap: true,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: isMobile ? 2 : 4,
crossAxisSpacing: 6,
mainAxisSpacing: 6,
childAspectRatio: 16.0 / 9.0,
),
itemCount: items.length,
itemBuilder: (context, index) => WelcomeQuestion(
question: items[index],
onSelected: onSelectedQuestion,
),
Wrap(
children: items
.map(
(i) => WelcomeQuestion(
question: i,
onSelected: onSelectedQuestion,
),
)
.toList(),
),
],
),
@@ -1,9 +1,10 @@
import 'package:flutter/material.dart';
import 'package:appflowy/plugins/base/emoji/emoji_picker_header.dart';
import 'package:appflowy/plugins/base/emoji/emoji_search_bar.dart';
import 'package:appflowy/plugins/base/emoji/emoji_skin_tone.dart';
import 'package:flowy_infra/size.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_emoji_mart/flutter_emoji_mart.dart';
// use a global value to store the selected emoji to prevent reloading every time.
@@ -37,9 +38,7 @@ class _FlowyEmojiPickerState extends State<FlowyEmojiPicker> {
EmojiData.builtIn().then(
(value) {
kCachedEmojiData = value;
setState(() {
emojiData = value;
});
setState(() => emojiData = value);
},
);
}
@@ -1,5 +1,7 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:appflowy/plugins/database/application/field/field_controller.dart';
import 'package:appflowy/plugins/database/application/view/view_cache.dart';
import 'package:appflowy/plugins/database/domain/database_view_service.dart';
@@ -12,7 +14,6 @@ import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_result/appflowy_result.dart';
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'defines.dart';
import 'row/row_cache.dart';
@@ -223,6 +224,7 @@ class DatabaseController {
_databaseCallbacks.clear();
_groupCallbacks.clear();
_layoutCallbacks.clear();
_isLoading.dispose();
}
Future<void> _loadGroups() async {
@@ -1,3 +1,5 @@
import 'package:flutter/material.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
@@ -19,12 +21,12 @@ import 'package:flowy_infra/size.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
import '../../application/row/row_controller.dart';
import '../../widgets/row/row_detail.dart';
import 'calendar_day.dart';
import 'layout/sizes.dart';
import 'toolbar/calendar_setting_bar.dart';
@@ -90,12 +92,11 @@ class _CalendarPageState extends State<CalendarPage> {
@override
void initState() {
super.initState();
_calendarState = GlobalKey<MonthViewState>();
_calendarBloc = CalendarBloc(
databaseController: widget.databaseController,
)..add(const CalendarEvent.initial());
super.initState();
}
@override
@@ -378,13 +379,7 @@ class UnscheduledEventsButton extends StatefulWidget {
}
class _UnscheduledEventsButtonState extends State<UnscheduledEventsButton> {
late final PopoverController _popoverController;
@override
void initState() {
super.initState();
_popoverController = PopoverController();
}
final PopoverController _popoverController = PopoverController();
@override
Widget build(BuildContext context) {
@@ -30,6 +30,12 @@ class CalendarLayoutSetting extends StatefulWidget {
class _CalendarLayoutSettingState extends State<CalendarLayoutSetting> {
final PopoverMutex popoverMutex = PopoverMutex();
@override
void dispose() {
popoverMutex.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return BlocProvider(
@@ -1,18 +1,19 @@
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/filter/checkbox_filter_editor_bloc.dart';
import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/checkbox_filter.pbenum.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/checkbox_filter.pbenum.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../condition_button.dart';
import '../disclosure_button.dart';
import '../filter_info.dart';
import 'choicechip.dart';
class CheckboxFilterChoicechip extends StatefulWidget {
@@ -30,9 +31,9 @@ class _CheckboxFilterChoicechipState extends State<CheckboxFilterChoicechip> {
@override
void initState() {
super.initState();
bloc = CheckboxFilterEditorBloc(filterInfo: widget.filterInfo)
..add(const CheckboxFilterEditorEvent.initial());
super.initState();
}
@override
@@ -82,6 +83,12 @@ class CheckboxFilterEditor extends StatefulWidget {
class _CheckboxFilterEditorState extends State<CheckboxFilterEditor> {
final popoverMutex = PopoverMutex();
@override
void dispose() {
popoverMutex.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return BlocProvider.value(
@@ -1,12 +1,14 @@
import 'package:flutter/material.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database/grid/application/filter/checklist_filter_bloc.dart';
import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/checklist_filter.pbenum.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/checklist_filter.pbenum.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../condition_button.dart';
import '../../disclosure_button.dart';
import '../../filter_info.dart';
@@ -23,20 +25,20 @@ class ChecklistFilterChoicechip extends StatefulWidget {
}
class _ChecklistFilterChoicechipState extends State<ChecklistFilterChoicechip> {
late ChecklistFilterEditorBloc bloc;
late PopoverMutex popoverMutex;
late final ChecklistFilterEditorBloc bloc;
final PopoverMutex popoverMutex = PopoverMutex();
@override
void initState() {
popoverMutex = PopoverMutex();
super.initState();
bloc = ChecklistFilterEditorBloc(filterInfo: widget.filterInfo);
bloc.add(const ChecklistFilterEditorEvent.initial());
super.initState();
}
@override
void dispose() {
bloc.close();
popoverMutex.dispose();
super.dispose();
}
@@ -1,3 +1,5 @@
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/filter/number_filter_editor_bloc.dart';
@@ -6,12 +8,12 @@ import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../condition_button.dart';
import '../disclosure_button.dart';
import '../filter_info.dart';
import 'choicechip.dart';
class NumberFilterChoiceChip extends StatefulWidget {
@@ -64,6 +66,12 @@ class NumberFilterEditor extends StatefulWidget {
class _NumberFilterEditorState extends State<NumberFilterEditor> {
final popoverMutex = PopoverMutex();
@override
void dispose() {
popoverMutex.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return BlocBuilder<NumberFilterEditorBloc, NumberFilterEditorState>(
@@ -1,14 +1,16 @@
import 'package:flutter/material.dart';
import 'package:appflowy/plugins/database/grid/application/filter/select_option_filter_bloc.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/select_option_filter.pb.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../disclosure_button.dart';
import '../../filter_info.dart';
import '../choicechip.dart';
import 'condition_list.dart';
import 'option_list.dart';
import 'select_option_loader.dart';
@@ -29,6 +31,7 @@ class _SelectOptionFilterChoicechipState
@override
void initState() {
super.initState();
if (widget.filterInfo.fieldInfo.fieldType == FieldType.SingleSelect) {
bloc = SelectOptionFilterEditorBloc(
filterInfo: widget.filterInfo,
@@ -43,7 +46,6 @@ class _SelectOptionFilterChoicechipState
);
}
bloc.add(const SelectOptionFilterEditorEvent.initial());
super.initState();
}
@override
@@ -90,6 +92,12 @@ class SelectOptionFilterEditor extends StatefulWidget {
class _SelectOptionFilterEditorState extends State<SelectOptionFilterEditor> {
final popoverMutex = PopoverMutex();
@override
void dispose() {
popoverMutex.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return BlocProvider.value(
@@ -1,3 +1,5 @@
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/filter/text_filter_editor_bloc.dart';
@@ -6,12 +8,12 @@ import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../condition_button.dart';
import '../disclosure_button.dart';
import '../filter_info.dart';
import 'choicechip.dart';
class TextFilterChoicechip extends StatelessWidget {
@@ -72,6 +74,12 @@ class TextFilterEditor extends StatefulWidget {
class _TextFilterEditorState extends State<TextFilterEditor> {
final popoverMutex = PopoverMutex();
@override
void dispose() {
popoverMutex.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return BlocBuilder<TextFilterEditorBloc, TextFilterEditorState>(
@@ -1,3 +1,5 @@
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/filter/time_filter_editor_bloc.dart';
@@ -6,12 +8,12 @@ import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../condition_button.dart';
import '../disclosure_button.dart';
import '../filter_info.dart';
import 'choicechip.dart';
class TimeFilterChoiceChip extends StatefulWidget {
@@ -64,6 +66,12 @@ class TimeFilterEditor extends StatefulWidget {
class _TimeFilterEditorState extends State<TimeFilterEditor> {
final popoverMutex = PopoverMutex();
@override
void dispose() {
popoverMutex.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return BlocBuilder<TimeFilterEditorBloc, TimeFilterEditorState>(
@@ -1,3 +1,5 @@
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/field/field_info.dart';
@@ -10,7 +12,6 @@ import 'package:flowy_infra_ui/style_widget/scrolling/styled_list.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flowy_infra_ui/style_widget/text_field.dart';
import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../../application/field/field_controller.dart';
@@ -35,15 +36,15 @@ class GridCreateFilterList extends StatefulWidget {
}
class _GridCreateFilterListState extends State<GridCreateFilterList> {
late GridCreateFilterBloc editBloc;
late final GridCreateFilterBloc editBloc;
@override
void initState() {
super.initState();
editBloc = GridCreateFilterBloc(
viewId: widget.viewId,
fieldController: widget.fieldController,
)..add(const GridCreateFilterEvent.initial());
super.initState();
}
@override
@@ -1,3 +1,5 @@
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/field/field_controller.dart';
@@ -5,9 +7,7 @@ import 'package:appflowy/plugins/database/grid/application/filter/filter_menu_bl
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:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'create_filter_list.dart';
@@ -77,13 +77,7 @@ class AddFilterButton extends StatefulWidget {
}
class _AddFilterButtonState extends State<AddFilterButton> {
late PopoverController popoverController;
@override
void initState() {
popoverController = PopoverController();
super.initState();
}
final PopoverController popoverController = PopoverController();
@override
Widget build(BuildContext context) {
@@ -1,3 +1,5 @@
import 'package:flutter/material.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/plugins/database/application/field/field_cell_bloc.dart';
import 'package:appflowy/plugins/database/application/field/field_controller.dart';
@@ -9,7 +11,6 @@ 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:flowy_infra_ui/style_widget/hover.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../layout/sizes.dart';
@@ -41,13 +42,12 @@ class GridFieldCell extends StatefulWidget {
}
class _GridFieldCellState extends State<GridFieldCell> {
final PopoverController popoverController = PopoverController();
late final FieldCellBloc _bloc;
late PopoverController popoverController;
@override
void initState() {
super.initState();
popoverController = PopoverController();
_bloc = FieldCellBloc(viewId: widget.viewId, fieldInfo: widget.fieldInfo);
if (widget.isEditing) {
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
@@ -1,5 +1,7 @@
import 'dart:io';
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/sort/sort_editor_bloc.dart';
@@ -7,10 +9,8 @@ import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/sort_entities.pbenum.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:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'create_sort_list.dart';
@@ -28,6 +28,12 @@ class SortEditor extends StatefulWidget {
class _SortEditorState extends State<SortEditor> {
final popoverMutex = PopoverMutex();
@override
void dispose() {
popoverMutex.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return BlocBuilder<SortEditorBloc, SortEditorState>(
@@ -1,8 +1,8 @@
import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';
import 'package:appflowy/plugins/database/widgets/cell_editor/extension.dart';
import 'package:appflowy/plugins/database/application/cell/bloc/select_option_cell_bloc.dart';
import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';
import 'package:appflowy/plugins/database/widgets/cell_editor/extension.dart';
import 'package:appflowy/plugins/database/widgets/cell_editor/select_option_cell_editor.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
@@ -56,7 +56,7 @@ class DesktopGridSelectOptionCellSkin extends IEditableSelectOptionCellSkin {
child: SelectOptionTag(
option: option,
padding: const EdgeInsets.symmetric(
vertical: 1,
vertical: 4,
horizontal: 8,
),
),
@@ -1,8 +1,8 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';
import 'package:appflowy/plugins/database/widgets/cell_editor/extension.dart';
import 'package:appflowy/plugins/database/application/cell/bloc/select_option_cell_bloc.dart';
import 'package:appflowy/plugins/database/widgets/cell_editor/extension.dart';
import 'package:appflowy/plugins/database/widgets/cell_editor/select_option_cell_editor.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart';
@@ -67,7 +67,7 @@ class DesktopRowDetailSelectOptionCellSkin
return SelectOptionTag(
option: option,
padding: const EdgeInsets.symmetric(
vertical: 1,
vertical: 4,
horizontal: 8,
),
);
@@ -96,7 +96,6 @@ class SelectOptionTag extends StatelessWidget {
);
return Container(
height: 20,
padding: onRemove == null ? padding : padding.copyWith(right: 2.0),
decoration: BoxDecoration(
color: optionColor,
@@ -314,13 +314,7 @@ class _SelectOptionCell extends StatefulWidget {
}
class _SelectOptionCellState extends State<_SelectOptionCell> {
late PopoverController _popoverController;
@override
void initState() {
_popoverController = PopoverController();
super.initState();
}
final _popoverController = PopoverController();
@override
Widget build(BuildContext context) {
@@ -480,11 +474,11 @@ class SelectOptionTagCell extends StatelessWidget {
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 6.0,
vertical: 4.0,
),
child: SelectOptionTag(
option: option,
padding: const EdgeInsets.symmetric(horizontal: 8),
padding:
const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
),
),
),
@@ -132,7 +132,10 @@ class _SelectOptionTextFieldState extends State<SelectOptionTextField> {
(option) => SelectOptionTag(
option: option,
onRemove: (option) => widget.onRemove(option),
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 1),
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 4,
),
),
)
.toList();
@@ -370,13 +370,7 @@ class FieldDetailsEditor extends StatefulWidget {
}
class _FieldDetailsEditorState extends State<FieldDetailsEditor> {
late PopoverMutex popoverMutex;
@override
void initState() {
popoverMutex = PopoverMutex();
super.initState();
}
final PopoverMutex popoverMutex = PopoverMutex();
@override
void dispose() {
@@ -575,10 +569,7 @@ class _FieldNameTextFieldState extends State<FieldNameTextField> {
}
class SwitchFieldButton extends StatefulWidget {
const SwitchFieldButton({
super.key,
required this.popoverMutex,
});
const SwitchFieldButton({super.key, required this.popoverMutex});
final PopoverMutex popoverMutex;
@@ -1,5 +1,8 @@
import 'dart:io';
import 'package:flutter/foundation.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';
@@ -18,13 +21,12 @@ 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.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'accessory/cell_accessory.dart';
import '../cell/editable_cell_builder.dart';
import 'accessory/cell_accessory.dart';
/// Display the row properties in a list. Only used in [RowDetailPage].
class RowPropertyList extends StatelessWidget {
const RowPropertyList({
@@ -363,15 +365,9 @@ class CreateRowFieldButton extends StatefulWidget {
}
class _CreateRowFieldButtonState extends State<CreateRowFieldButton> {
late PopoverController popoverController;
final PopoverController popoverController = PopoverController();
FieldPB? createdField;
@override
void initState() {
popoverController = PopoverController();
super.initState();
}
@override
Widget build(BuildContext context) {
return AppFlowyPopover(
@@ -1,3 +1,5 @@
import 'package:flutter/widgets.dart';
import 'package:appflowy/plugins/database/application/database_controller.dart';
import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';
import 'package:appflowy/plugins/database/widgets/setting/database_setting_action.dart';
@@ -6,7 +8,6 @@ import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:flowy_infra_ui/style_widget/scrolling/styled_list.dart';
import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:flutter/widgets.dart';
class DatabaseSettingsList extends StatefulWidget {
const DatabaseSettingsList({
@@ -21,7 +22,13 @@ class DatabaseSettingsList extends StatefulWidget {
}
class _DatabaseSettingsListState extends State<DatabaseSettingsList> {
late final PopoverMutex popoverMutex = PopoverMutex();
final PopoverMutex popoverMutex = PopoverMutex();
@override
void dispose() {
popoverMutex.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
@@ -1,5 +1,7 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/plugins/database/application/field/field_controller.dart';
import 'package:appflowy/plugins/database/application/field/field_info.dart';
@@ -13,7 +15,6 @@ import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:collection/collection.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class DatabasePropertyList extends StatefulWidget {
@@ -43,6 +44,12 @@ class _DatabasePropertyListState extends State<DatabasePropertyList> {
)..add(const DatabasePropertyEvent.initial());
}
@override
void dispose() {
_popoverMutex.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return BlocProvider<DatabasePropertyBloc>.value(
@@ -8,7 +8,6 @@ import 'package:appflowy/plugins/document/application/document_data_pb_extension
import 'package:appflowy/plugins/document/application/document_listener.dart';
import 'package:appflowy/plugins/document/application/document_service.dart';
import 'package:appflowy/plugins/document/application/editor_transaction_adapter.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/migration/editor_migration.dart';
import 'package:appflowy/plugins/trash/application/trash_service.dart';
import 'package:appflowy/shared/feature_flags.dart';
import 'package:appflowy/startup/startup.dart';
@@ -19,7 +18,6 @@ import 'package:appflowy/util/color_to_hex_string.dart';
import 'package:appflowy/util/debounce.dart';
import 'package:appflowy/util/throttle.dart';
import 'package:appflowy/workspace/application/view/view_listener.dart';
import 'package:appflowy/workspace/application/view/view_service.dart';
import 'package:appflowy_backend/protobuf/flowy-document/entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-document/protobuf.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
@@ -118,11 +116,6 @@ class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
final result = await _fetchDocumentState();
_onViewChanged();
_onDocumentChanged();
result.onSuccess((s) {
if (s != null) {
_migrateCover(s);
}
});
final newState = await result.fold(
(s) async {
final userProfilePB =
@@ -390,14 +383,6 @@ class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
metadata: jsonEncode(metadata.toJson()),
);
}
// from version 0.5.5, the cover is stored in the view.ext
Future<void> _migrateCover(EditorState editorState) async {
final view = await ViewBackendService.getView(documentId);
view.onSuccess((s) {
return EditorMigration.migrateCoverIfNeeded(s, editorState);
});
}
}
@freezed
@@ -1,19 +1,21 @@
import 'dart:io';
import 'package:appflowy/user/application/user_service.dart';
import 'package:appflowy/workspace/application/export/document_exporter.dart';
import 'package:appflowy/workspace/application/view/view_listener.dart';
import 'package:appflowy/workspace/application/view/view_service.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-document/entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:appflowy_result/appflowy_result.dart';
import 'package:flowy_infra/uuid.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'document_share_bloc.freezed.dart';
const _url = 'https://test.appflowy.io';
const _url = 'https://appflowy.com';
class DocumentShareBloc extends Bloc<DocumentShareEvent, DocumentShareState> {
DocumentShareBloc({
@@ -22,17 +24,17 @@ class DocumentShareBloc extends Bloc<DocumentShareEvent, DocumentShareState> {
on<DocumentShareEvent>((event, emit) async {
await event.when(
initial: () async {
final publishInfo = await ViewBackendService.getPublishInfo(view);
publishInfo.fold((s) {
emit(
state.copyWith(
isPublished: true,
url: '$_url/${s.namespace}/${s.publishName}',
),
viewListener = ViewListener(viewId: view.id)
..start(
onViewUpdated: (value) {
add(DocumentShareEvent.updateViewName(value.name));
},
onViewMoveToTrash: (p0) {
add(const DocumentShareEvent.setPublishStatus(false));
},
);
}, (f) {
emit(state.copyWith(isPublished: false, url: ''));
});
add(const DocumentShareEvent.updatePublishStatus());
},
share: (type, path) async {
if (DocumentShareType.unimplemented.contains(type)) {
@@ -51,27 +53,23 @@ class DocumentShareBloc extends Bloc<DocumentShareEvent, DocumentShareState> {
),
);
},
publish: (url) async {
// todo: optimize the logic
const prefix = 'appflowy';
final name = '${view.name}-${uuid()}'.substring(0, 19);
publish: (nameSpace, publishName) async {
// set space name
try {
final nameSpace =
final result =
await ViewBackendService.getPublishNameSpace().getOrThrow();
if (nameSpace.namespace != prefix) {
await ViewBackendService.setPublishNameSpace(prefix).getOrThrow();
}
final result = await ViewBackendService.publish(view, name: name);
await ViewBackendService.publish(
view,
name: publishName,
).getOrThrow();
emit(
state.copyWith(
isPublished: true,
publishResult: result,
url: '$_url/${nameSpace.namespace}/$name',
publishResult: FlowySuccess(null),
unpublishResult: null,
url: '$_url/${result.namespace}/$publishName',
),
);
} catch (e) {
@@ -83,29 +81,88 @@ class DocumentShareBloc extends Bloc<DocumentShareEvent, DocumentShareState> {
publishResult: FlowyResult.failure(
FlowyError(msg: 'publish error: $e'),
),
unpublishResult: null,
url: '',
),
);
}
},
unPublish: () async {
await ViewBackendService.unpublish(view);
emit(
state.copyWith(
isPublished: false,
publishResult: null,
url: '',
unpublishResult: null,
),
);
final result = await ViewBackendService.unpublish(view);
final isPublished = !result.isSuccess;
result.onFailure((f) {
Log.error('unpublish error: $f');
});
emit(
state.copyWith(
isPublished: isPublished,
publishResult: null,
unpublishResult: result,
url: result.fold((_) => '', (_) => state.url),
),
);
},
updateViewName: (viewName) async {
emit(state.copyWith(viewName: viewName));
},
setPublishStatus: (isPublished) {
emit(
state.copyWith(
isPublished: isPublished,
url: isPublished ? state.url : '',
),
);
},
updatePublishStatus: () async {
final publishInfo = await ViewBackendService.getPublishInfo(view);
final enablePublish =
await UserBackendService.getCurrentUserProfile().fold(
(v) => v.authenticator == AuthenticatorPB.AppFlowyCloud,
(p) => false,
);
publishInfo.fold((s) {
emit(
state.copyWith(
isPublished: true,
url: '$_url/${s.namespace}/${s.publishName}',
viewName: view.name,
enablePublish: enablePublish,
),
);
}, (f) {
emit(
state.copyWith(
isPublished: false,
url: '',
viewName: view.name,
enablePublish: enablePublish,
),
);
});
},
);
});
}
final ViewPB view;
late final ViewListener viewListener;
late final exporter = DocumentExporter(view);
@override
Future<void> close() async {
await viewListener.stop();
return super.close();
}
Future<FlowyResult<ExportDataPB, FlowyError>> _export(
DocumentShareType type,
String? path,
@@ -173,23 +230,36 @@ class DocumentShareEvent with _$DocumentShareEvent {
DocumentShareType type,
String? path,
) = _Share;
const factory DocumentShareEvent.publish(String url) = _Publish;
const factory DocumentShareEvent.publish(
String nameSpace,
String pageId,
) = _Publish;
const factory DocumentShareEvent.unPublish() = _UnPublish;
const factory DocumentShareEvent.updateViewName(String name) =
_UpdateViewName;
const factory DocumentShareEvent.updatePublishStatus() = _UpdatePublishStatus;
const factory DocumentShareEvent.setPublishStatus(bool isPublished) =
_SetPublishStatus;
}
@freezed
class DocumentShareState with _$DocumentShareState {
const factory DocumentShareState({
required bool isLoading,
FlowyResult<ExportDataPB, FlowyError>? exportResult,
required bool isPublished,
FlowyResult<void, FlowyError>? publishResult,
required bool isLoading,
required String url,
required String viewName,
required bool enablePublish,
FlowyResult<ExportDataPB, FlowyError>? exportResult,
FlowyResult<void, FlowyError>? publishResult,
FlowyResult<void, FlowyError>? unpublishResult,
}) = _DocumentShareState;
factory DocumentShareState.initial() => const DocumentShareState(
isLoading: false,
isPublished: false,
enablePublish: true,
url: '',
viewName: '',
);
}
@@ -1,6 +1,3 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart';
import 'package:appflowy/plugins/document/presentation/editor_page.dart';
@@ -13,6 +10,8 @@ import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';
import 'package:easy_localization/easy_localization.dart' hide TextDirection;
import 'package:flowy_infra/theme_extension.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
Map<String, BlockComponentBuilder> getEditorBuilderMap({
@@ -1,8 +1,5 @@
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/application/document_bloc.dart';
import 'package:appflowy/plugins/document/presentation/editor_configuration.dart';
@@ -31,6 +28,8 @@ 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.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
final codeBlockLocalization = CodeBlockLocalizations(
@@ -215,7 +214,11 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
AFFocusManager? focusManager;
void _loseFocus() => widget.editorState.selection = null;
void _loseFocus() {
if (!widget.editorState.isDisposed) {
widget.editorState.selection = null;
}
}
@override
void initState() {
@@ -196,6 +196,7 @@ class _CalloutBlockComponentWidgetState
), // force to refresh the popover state
title: '',
emoji: emoji,
emojiSize: 16.0,
onSubmitted: (emoji, controller) {
setEmoji(emoji);
controller?.close();
@@ -204,7 +205,7 @@ class _CalloutBlockComponentWidgetState
),
Flexible(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
padding: const EdgeInsets.symmetric(vertical: 6.0),
child: buildCalloutBlockComponent(context, textDirection),
),
),
@@ -1,9 +1,10 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/material.dart';
/// Copy.
///
@@ -1,3 +1,6 @@
import 'package:flutter/material.dart';
import 'package:appflowy/plugins/document/application/document_bloc.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/editor_state_paste_node_extension.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_html.dart';
@@ -7,11 +10,9 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_p
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:string_validator/string_validator.dart';
/// Paste.
///
/// - support
/// - desktop
/// - web
@@ -41,8 +42,7 @@ CommandShortcutEventHandler _pasteCommandHandler = (editorState) {
final image = data.image;
// paste as link preview
final result = await _pasteAsLinkPreview(editorState, plainText);
if (result) {
if (await _pasteAsLinkPreview(editorState, plainText)) {
return;
}
@@ -55,23 +55,31 @@ CommandShortcutEventHandler _pasteCommandHandler = (editorState) {
// try to paste the content in order, if any of them is failed, then try the next one
if (inAppJson != null && inAppJson.isNotEmpty) {
await editorState.deleteSelectionIfNeeded();
final result = await editorState.pasteInAppJson(inAppJson);
if (result) {
if (await editorState.pasteInAppJson(inAppJson)) {
return;
}
}
if (html != null && html.isNotEmpty) {
await editorState.deleteSelectionIfNeeded();
final result = await editorState.pasteHtml(html);
if (result) {
if (await editorState.pasteHtml(html)) {
return;
}
}
if (image != null && image.$2?.isNotEmpty == true) {
final documentBloc =
editorState.document.root.context?.read<DocumentBloc>();
final documentId = documentBloc?.documentId;
if (documentId == null || documentId.isEmpty) {
return;
}
await editorState.deleteSelectionIfNeeded();
final result = await editorState.pasteImage(image.$1, image.$2!);
final result = await editorState.pasteImage(
image.$1,
image.$2!,
documentId,
);
if (result) {
return;
}
@@ -1,5 +1,11 @@
import 'package:appflowy_editor/appflowy_editor.dart';
final _listTypes = [
BulletedListBlockKeys.type,
TodoListBlockKeys.type,
NumberedListBlockKeys.type,
];
extension PasteNodes on EditorState {
Future<void> pasteSingleLineNode(Node insertedNode) async {
final selection = await deleteSelectionIfNeeded();
@@ -28,6 +34,19 @@ extension PasteNodes on EditorState {
offset: offset,
),
);
} else if (_listTypes.contains(node.type)) {
final convertedNode = insertedNode.copyWith(type: node.type);
final path = selection.start.path;
transaction
..insertNode(path, convertedNode)
..deleteNodesAtPath(path);
// Set the afterSelection to the last child of the inserted node
final lastChildPath = calculatePath(path, [convertedNode]);
final lastChildOffset = calculateLength([convertedNode]);
transaction.afterSelection = Selection.collapsed(
Position(path: lastChildPath, offset: lastChildOffset),
);
} else if (insertedDelta != null) {
// if the node is not empty, insert the delta from inserted node after the selection.
transaction.insertTextDelta(node, selection.endIndex, insertedDelta);
@@ -21,7 +21,11 @@ extension PasteFromImage on EditorState {
'gif',
];
Future<bool> pasteImage(String format, Uint8List imageBytes) async {
Future<bool> pasteImage(
String format,
Uint8List imageBytes,
String documentId,
) async {
if (!supportedImageFormats.contains(format)) {
return false;
}
@@ -56,7 +60,7 @@ extension PasteFromImage on EditorState {
if (isLocalMode) {
path = await saveImageToLocalStorage(copyToPath);
} else {
final result = await saveImageToCloudStorage(copyToPath);
final result = await saveImageToCloudStorage(copyToPath, documentId);
final errorMessage = result.$2;
@@ -91,6 +91,7 @@ class _DocumentCoverWidgetState extends State<DocumentCoverWidget> {
String viewIcon = '';
PageStyleCover? cover;
late ViewPB view;
late final ViewListener viewListener;
@override
@@ -99,6 +100,7 @@ class _DocumentCoverWidgetState extends State<DocumentCoverWidget> {
final value = widget.view.icon.value;
viewIcon = value.isNotEmpty ? value : icon ?? '';
cover = widget.view.cover;
view = widget.view;
widget.node.addListener(_reload);
viewListener = ViewListener(
viewId: widget.view.id,
@@ -107,6 +109,7 @@ class _DocumentCoverWidgetState extends State<DocumentCoverWidget> {
setState(() {
viewIcon = p0.icon.value;
cover = p0.cover;
view = p0;
});
},
);
@@ -137,7 +140,7 @@ class _DocumentCoverWidgetState extends State<DocumentCoverWidget> {
),
if (hasCover)
DocumentCover(
view: widget.view,
view: view,
editorState: widget.editorState,
node: widget.node,
coverType: coverType,
@@ -204,7 +207,7 @@ class _DocumentCoverWidgetState extends State<DocumentCoverWidget> {
// compatible with version > 0.5.5.
EditorMigration.migrateCoverIfNeeded(
widget.view,
widget.editorState,
attributes,
overwrite: true,
);
}
@@ -638,7 +641,7 @@ class DocumentCoverState extends State<DocumentCover> {
details = await saveImageToLocalStorage(details);
} else {
// else we should save the image to cloud storage
(details, _) = await saveImageToCloudStorage(details);
(details, _) = await saveImageToCloudStorage(details, widget.view.id);
}
}
widget.onChangeCover(type, details);
@@ -90,7 +90,7 @@ class ImagePlaceholderState extends State<ImagePlaceholder> {
UploadImageType.local,
UploadImageType.url,
UploadImageType.unsplash,
UploadImageType.openAI,
// UploadImageType.openAI,
UploadImageType.stabilityAI,
],
onSelectedLocalImage: (path) {
@@ -216,12 +216,16 @@ class ImagePlaceholderState extends State<ImagePlaceholder> {
// don't limit the image size for local mode.
path = await saveImageToLocalStorage(url);
} else {
final documentId = context.read<DocumentBloc>().documentId;
if (documentId.isEmpty) {
return;
}
// else we should save the image to cloud storage
setState(() {
showLoading = true;
this.errorMessage = null;
});
(path, errorMessage) = await saveImageToCloudStorage(url);
(path, errorMessage) = await saveImageToCloudStorage(url, documentId);
setState(() {
showLoading = false;
this.errorMessage = errorMessage;
@@ -39,6 +39,7 @@ Future<String?> saveImageToLocalStorage(String localImagePath) async {
Future<(String? path, String? errorMessage)> saveImageToCloudStorage(
String localImagePath,
String documentId,
) async {
final size = localImagePath.fileSize;
if (size == null || size > 10 * 1024 * 1024) {
@@ -52,8 +53,7 @@ Future<(String? path, String? errorMessage)> saveImageToCloudStorage(
Log.debug("Uploading image local path: $localImagePath");
final result = await documentService.uploadFile(
localFilePath: localImagePath,
// TODO(lucas): replace with actual documentId
documentId: "temp",
documentId: documentId,
);
return result.fold(
(s) async {
@@ -1,8 +1,8 @@
import 'dart:async';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/ai_client.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/error.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/openai_client.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy_result/appflowy_result.dart';
import 'package:easy_localization/easy_localization.dart';
@@ -22,7 +22,7 @@ class OpenAIImageWidget extends StatefulWidget {
}
class _OpenAIImageWidgetState extends State<OpenAIImageWidget> {
Future<FlowyResult<List<String>, OpenAIError>>? future;
Future<FlowyResult<List<String>, AIError>>? future;
String query = '';
@override
@@ -93,7 +93,7 @@ class _OpenAIImageWidgetState extends State<OpenAIImageWidget> {
}
void _search() async {
final openAI = await getIt.getAsync<OpenAIRepository>();
final openAI = await getIt.getAsync<AIRepository>();
setState(() {
future = openAI.generateImage(
prompt: query,
@@ -1,7 +1,6 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/cover_editor.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/embed_image_url_widget.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/open_ai_image_widget.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/stability_ai_image_widget.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/unsplash_image_widget.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/upload_image_file_widget.dart';
@@ -19,7 +18,7 @@ enum UploadImageType {
url,
unsplash,
stabilityAI,
openAI,
// openAI,
color;
String get description {
@@ -30,8 +29,8 @@ enum UploadImageType {
return LocaleKeys.document_imageBlock_embedLink_label.tr();
case UploadImageType.unsplash:
return LocaleKeys.document_imageBlock_unsplash_label.tr();
case UploadImageType.openAI:
return LocaleKeys.document_imageBlock_ai_label.tr();
// case UploadImageType.openAI:
// return LocaleKeys.document_imageBlock_ai_label.tr();
case UploadImageType.stabilityAI:
return LocaleKeys.document_imageBlock_stability_ai_label.tr();
case UploadImageType.color:
@@ -186,23 +185,23 @@ class _UploadImageMenuState extends State<UploadImageMenu> {
),
),
);
case UploadImageType.openAI:
return supportOpenAI
? Expanded(
child: Container(
padding: const EdgeInsets.all(8.0),
constraints: constraints,
child: OpenAIImageWidget(
onSelectNetworkImage: widget.onSelectedAIImage,
),
),
)
: Padding(
padding: const EdgeInsets.all(8.0),
child: FlowyText(
LocaleKeys.document_imageBlock_pleaseInputYourOpenAIKey.tr(),
),
);
// case UploadImageType.openAI:
// return supportOpenAI
// ? Expanded(
// child: Container(
// padding: const EdgeInsets.all(8.0),
// constraints: constraints,
// child: OpenAIImageWidget(
// onSelectNetworkImage: widget.onSelectedAIImage,
// ),
// ),
// )
// : Padding(
// padding: const EdgeInsets.all(8.0),
// child: FlowyText(
// LocaleKeys.document_imageBlock_pleaseInputYourOpenAIKey.tr(),
// ),
// );
case UploadImageType.stabilityAI:
return supportStabilityAI
? Expanded(
@@ -1,3 +1,5 @@
import 'package:flutter/material.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart';
@@ -25,7 +27,6 @@ import 'package:collection/collection.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:fixnum/fixnum.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:nanoid/non_secure.dart';
@@ -64,6 +65,12 @@ class _MentionDateBlockState extends State<MentionDateBlock> {
late bool _includeTime = widget.includeTime;
late DateTime? parsedDate = DateTime.tryParse(widget.date);
@override
void dispose() {
mutex.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
if (parsedDate == null) {
@@ -164,18 +164,17 @@ class EditorMigration {
// Now, the cover is stored in the view.ext.
static void migrateCoverIfNeeded(
ViewPB view,
EditorState editorState, {
Attributes attributes, {
bool overwrite = false,
}) async {
if (view.extra.isNotEmpty && !overwrite) {
return;
}
final root = editorState.document.root;
final coverType = CoverType.fromString(
root.attributes[DocumentHeaderBlockKeys.coverType],
attributes[DocumentHeaderBlockKeys.coverType],
);
final coverDetails = root.attributes[DocumentHeaderBlockKeys.coverDetails];
final coverDetails = attributes[DocumentHeaderBlockKeys.coverDetails];
Map extra = {};
@@ -1,5 +1,6 @@
import 'dart:async';
import 'dart:io';
import 'dart:math';
import 'package:appflowy/plugins/document/application/document_bloc.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_close_keyboard_or_menu_button.dart';
@@ -9,6 +10,7 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_too
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:collection/collection.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@@ -284,6 +286,14 @@ class _MobileToolbarState extends State<_MobileToolbar>
if (canUpdateCachedKeyboardHeight) {
cachedKeyboardHeight.value = height;
if (defaultTargetPlatform == TargetPlatform.android) {
// cache the keyboard height with the view padding in Android
if (cachedKeyboardHeight.value != 0) {
cachedKeyboardHeight.value +=
MediaQuery.of(context).viewPadding.bottom;
}
}
}
if (height == 0) {
@@ -385,13 +395,19 @@ class _MobileToolbarState extends State<_MobileToolbar>
return ValueListenableBuilder(
valueListenable: showMenuNotifier,
builder: (_, showingMenu, __) {
var paddingHeight = height;
if (Platform.isAndroid) {
paddingHeight =
height + MediaQuery.of(context).viewPadding.bottom;
var keyboardHeight = height;
if (defaultTargetPlatform == TargetPlatform.android) {
if (!showingMenu) {
// take the max value of the keyboard height and the view padding
// to make sure the toolbar is above the keyboard
keyboardHeight = max(
keyboardHeight,
MediaQuery.of(context).viewInsets.bottom,
);
}
}
return SizedBox(
height: paddingHeight,
height: keyboardHeight,
child: (showingMenu && selectedMenuIndex != null)
? widget.toolbarItems[selectedMenuIndex!].menuBuilder?.call(
context,
@@ -0,0 +1,32 @@
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/error.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/text_completion.dart';
import 'package:appflowy_backend/protobuf/flowy-chat/entities.pb.dart';
import 'package:appflowy_result/appflowy_result.dart';
abstract class AIRepository {
Future<void> getStreamedCompletions({
required String prompt,
required Future<void> Function() onStart,
required Future<void> Function(TextCompletionResponse response) onProcess,
required Future<void> Function() onEnd,
required void Function(AIError error) onError,
String? suffix,
int maxTokens = 2048,
double temperature = 0.3,
bool useAction = false,
});
Future<void> streamCompletion({
required String text,
required CompletionTypePB completionType,
required Future<void> Function() onStart,
required Future<void> Function(String text) onProcess,
required Future<void> Function() onEnd,
required void Function(AIError error) onError,
});
Future<FlowyResult<List<String>, AIError>> generateImage({
required String prompt,
int n = 1,
});
}
@@ -3,12 +3,12 @@ part 'error.freezed.dart';
part 'error.g.dart';
@freezed
class OpenAIError with _$OpenAIError {
const factory OpenAIError({
class AIError with _$AIError {
const factory AIError({
String? code,
required String message,
}) = _OpenAIError;
}) = _AIError;
factory OpenAIError.fromJson(Map<String, Object?> json) =>
_$OpenAIErrorFromJson(json);
factory AIError.fromJson(Map<String, Object?> json) =>
_$AIErrorFromJson(json);
}
@@ -1,7 +1,8 @@
import 'dart:async';
import 'dart:convert';
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/text_edit.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/ai_client.dart';
import 'package:appflowy_backend/protobuf/flowy-chat/entities.pbenum.dart';
import 'package:appflowy_result/appflowy_result.dart';
import 'package:http/http.dart' as http;
@@ -25,58 +26,7 @@ enum OpenAIRequestType {
}
}
abstract class OpenAIRepository {
/// Get completions from GPT-3
///
/// [prompt] is the prompt text
/// [suffix] is the suffix text
/// [maxTokens] is the maximum number of tokens to generate
/// [temperature] is the temperature of the model
///
Future<FlowyResult<TextCompletionResponse, OpenAIError>> getCompletions({
required String prompt,
String? suffix,
int maxTokens = 2048,
double temperature = .3,
});
Future<void> getStreamedCompletions({
required String prompt,
required Future<void> Function() onStart,
required Future<void> Function(TextCompletionResponse response) onProcess,
required Future<void> Function() onEnd,
required void Function(OpenAIError error) onError,
String? suffix,
int maxTokens = 2048,
double temperature = 0.3,
bool useAction = false,
});
/// Get edits from GPT-3
///
/// [input] is the input text
/// [instruction] is the instruction text
/// [temperature] is the temperature of the model
///
Future<FlowyResult<TextEditResponse, OpenAIError>> getEdits({
required String input,
required String instruction,
double temperature = 0.3,
});
/// Generate image from GPT-3
///
/// [prompt] is the prompt text
/// [n] is the number of images to generate
///
/// the result is a list of urls
Future<FlowyResult<List<String>, OpenAIError>> generateImage({
required String prompt,
int n = 1,
});
}
class HttpOpenAIRepository implements OpenAIRepository {
class HttpOpenAIRepository implements AIRepository {
const HttpOpenAIRepository({
required this.client,
required this.apiKey,
@@ -90,50 +40,13 @@ class HttpOpenAIRepository implements OpenAIRepository {
'Content-Type': 'application/json',
};
@override
Future<FlowyResult<TextCompletionResponse, OpenAIError>> getCompletions({
required String prompt,
String? suffix,
int maxTokens = 2048,
double temperature = 0.3,
}) async {
final parameters = {
'model': 'gpt-3.5-turbo-instruct',
'prompt': prompt,
'suffix': suffix,
'max_tokens': maxTokens,
'temperature': temperature,
'stream': false,
};
final response = await client.post(
OpenAIRequestType.textCompletion.uri,
headers: headers,
body: json.encode(parameters),
);
if (response.statusCode == 200) {
return FlowyResult.success(
TextCompletionResponse.fromJson(
json.decode(
utf8.decode(response.bodyBytes),
),
),
);
} else {
return FlowyResult.failure(
OpenAIError.fromJson(json.decode(response.body)['error']),
);
}
}
@override
Future<void> getStreamedCompletions({
required String prompt,
required Future<void> Function() onStart,
required Future<void> Function(TextCompletionResponse response) onProcess,
required Future<void> Function() onEnd,
required void Function(OpenAIError error) onError,
required void Function(AIError error) onError,
String? suffix,
int maxTokens = 2048,
double temperature = 0.3,
@@ -201,50 +114,14 @@ class HttpOpenAIRepository implements OpenAIRepository {
} else {
final body = await response.stream.bytesToString();
onError(
OpenAIError.fromJson(json.decode(body)['error']),
AIError.fromJson(json.decode(body)['error']),
);
}
return;
}
@override
Future<FlowyResult<TextEditResponse, OpenAIError>> getEdits({
required String input,
required String instruction,
double temperature = 0.3,
int n = 1,
}) async {
final parameters = {
'model': 'gpt-4',
'input': input,
'instruction': instruction,
'temperature': temperature,
'n': n,
};
final response = await client.post(
OpenAIRequestType.textEdit.uri,
headers: headers,
body: json.encode(parameters),
);
if (response.statusCode == 200) {
return FlowyResult.success(
TextEditResponse.fromJson(
json.decode(
utf8.decode(response.bodyBytes),
),
),
);
} else {
return FlowyResult.failure(
OpenAIError.fromJson(json.decode(response.body)['error']),
);
}
}
@override
Future<FlowyResult<List<String>, OpenAIError>> generateImage({
Future<FlowyResult<List<String>, AIError>> generateImage({
required String prompt,
int n = 1,
}) async {
@@ -273,11 +150,23 @@ class HttpOpenAIRepository implements OpenAIRepository {
return FlowyResult.success(urls);
} else {
return FlowyResult.failure(
OpenAIError.fromJson(json.decode(response.body)['error']),
AIError.fromJson(json.decode(response.body)['error']),
);
}
} catch (error) {
return FlowyResult.failure(OpenAIError(message: error.toString()));
return FlowyResult.failure(AIError(message: error.toString()));
}
}
@override
Future<void> streamCompletion({
required String text,
required CompletionTypePB completionType,
required Future<void> Function() onStart,
required Future<void> Function(String text) onProcess,
required Future<void> Function() onEnd,
required void Function(AIError error) onError,
}) {
throw UnimplementedError();
}
}
@@ -1,22 +1,20 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/build_context_extension.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/text_robot.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/openai_client.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/util/learn_more_action.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/widgets/discard_dialog.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/widgets/loading.dart';
import 'package:appflowy/user/application/ai_service.dart';
import 'package:appflowy/user/application/user_service.dart';
import 'package:appflowy/workspace/presentation/home/toast.dart';
import 'package:appflowy_backend/protobuf/flowy-chat/entities.pb.dart';
import 'package:appflowy_editor/appflowy_editor.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';
import 'package:flowy_infra_ui/style_widget/text_field.dart';
import 'package:flowy_infra_ui/widget/buttons/primary_button.dart';
import 'package:flowy_infra_ui/widget/buttons/secondary_button.dart';
import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:provider/provider.dart';
class AutoCompletionBlockKeys {
@@ -187,15 +185,11 @@ class _AutoCompletionBlockComponentState
}
Future<void> _onGenerate() async {
final loading = Loading(context);
await loading.start();
await _updateEditingText();
final userProfile = await UserBackendService.getCurrentUserProfile()
.then((value) => value.toNullable());
if (userProfile == null) {
await loading.stop();
if (mounted) {
showSnackBarMessage(
context,
@@ -208,34 +202,28 @@ class _AutoCompletionBlockComponentState
final textRobot = TextRobot(editorState: editorState);
BarrierDialog? barrierDialog;
final openAIRepository = HttpOpenAIRepository(
client: http.Client(),
apiKey: userProfile.openaiKey,
);
await openAIRepository.getStreamedCompletions(
prompt: controller.text,
final aiRepository = AppFlowyAIService();
await aiRepository.streamCompletion(
text: controller.text,
completionType: CompletionTypePB.ContinueWriting,
onStart: () async {
await loading.stop();
if (mounted) {
barrierDialog = BarrierDialog(context);
await barrierDialog?.show();
barrierDialog?.show();
await _makeSurePreviousNodeIsEmptyParagraphNode();
}
},
onProcess: (response) async {
if (response.choices.isNotEmpty) {
final text = response.choices.first.text;
await textRobot.autoInsertText(
text,
delay: Duration.zero,
);
}
onProcess: (text) async {
await textRobot.autoInsertText(
text,
delay: Duration.zero,
);
},
onEnd: () async {
await barrierDialog?.dismiss();
barrierDialog?.dismiss();
},
onError: (error) async {
await loading.stop();
barrierDialog?.dismiss();
if (mounted) {
showSnackBarMessage(
context,
@@ -272,8 +260,6 @@ class _AutoCompletionBlockComponentState
return;
}
final loading = Loading(context);
await loading.start();
// clear previous response
final selection = startSelection;
if (selection != null) {
@@ -292,7 +278,6 @@ class _AutoCompletionBlockComponentState
final userProfile = await UserBackendService.getCurrentUserProfile()
.then((value) => value.toNullable());
if (userProfile == null) {
await loading.stop();
if (mounted) {
showSnackBarMessage(
context,
@@ -303,28 +288,21 @@ class _AutoCompletionBlockComponentState
return;
}
final textRobot = TextRobot(editorState: editorState);
final openAIRepository = HttpOpenAIRepository(
client: http.Client(),
apiKey: userProfile.openaiKey,
);
await openAIRepository.getStreamedCompletions(
prompt: _rewritePrompt(previousOutput),
final aiService = AppFlowyAIService();
await aiService.streamCompletion(
text: _rewritePrompt(previousOutput),
completionType: CompletionTypePB.ContinueWriting,
onStart: () async {
await loading.stop();
await _makeSurePreviousNodeIsEmptyParagraphNode();
},
onProcess: (response) async {
if (response.choices.isNotEmpty) {
final text = response.choices.first.text;
await textRobot.autoInsertText(
text,
delay: Duration.zero,
);
}
onProcess: (text) async {
await textRobot.autoInsertText(
text,
delay: Duration.zero,
);
},
onEnd: () async {},
onError: (error) async {
await loading.stop();
if (mounted) {
showSnackBarMessage(
context,
@@ -462,23 +440,9 @@ class AutoCompletionHeader extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Row(
children: [
FlowyText.medium(
LocaleKeys.document_plugins_autoGeneratorTitleName.tr(),
fontSize: 14,
),
const Spacer(),
FlowyButton(
useIntrinsicWidth: true,
text: FlowyText.regular(
LocaleKeys.document_plugins_autoGeneratorLearnMore.tr(),
),
onTap: () async {
await openLearnMorePage();
},
),
],
return FlowyText.medium(
LocaleKeys.document_plugins_autoGeneratorTitleName.tr(),
fontSize: 14,
);
}
}
@@ -1,3 +1,5 @@
import 'dart:async';
import 'package:flutter/material.dart';
class Loading {
@@ -6,25 +8,27 @@ class Loading {
BuildContext? loadingContext;
final BuildContext context;
Future<void> start() async => showDialog<void>(
context: context,
barrierDismissible: false,
builder: (BuildContext context) {
loadingContext = context;
return const SimpleDialog(
elevation: 0.0,
backgroundColor:
Colors.transparent, // can change this to your preferred color
children: [
Center(
child: CircularProgressIndicator(),
),
],
);
},
void start() => unawaited(
showDialog<void>(
context: context,
barrierDismissible: false,
builder: (BuildContext context) {
loadingContext = context;
return const SimpleDialog(
elevation: 0.0,
backgroundColor:
Colors.transparent, // can change this to your preferred color
children: [
Center(
child: CircularProgressIndicator(),
),
],
);
},
),
);
Future<void> stop() async {
void stop() {
if (loadingContext != null) {
Navigator.of(loadingContext!).pop();
loadingContext = null;
@@ -38,15 +42,17 @@ class BarrierDialog {
late BuildContext loadingContext;
final BuildContext context;
Future<void> show() async => showDialog<void>(
context: context,
barrierDismissible: false,
barrierColor: Colors.transparent,
builder: (BuildContext context) {
loadingContext = context;
return const SizedBox.shrink();
},
void show() => unawaited(
showDialog<void>(
context: context,
barrierDismissible: false,
barrierColor: Colors.transparent,
builder: (BuildContext context) {
loadingContext = context;
return const SizedBox.shrink();
},
),
);
Future<void> dismiss() async => Navigator.of(loadingContext).pop();
void dismiss() => Navigator.of(loadingContext).pop();
}
@@ -1,18 +1,18 @@
import 'dart:async';
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/openai_client.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/util/learn_more_action.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/ai_client.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/widgets/discard_dialog.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/widgets/smart_edit_action.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/application/ai_service.dart';
import 'package:appflowy/workspace/presentation/home/toast.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/style_widget/decoration.dart';
import 'package:flutter/material.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:http/http.dart' as http;
import 'package:provider/provider.dart';
@@ -229,7 +229,11 @@ class _SmartEditInputWidgetState extends State<SmartEditInputWidget> {
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildHeaderWidget(context),
FlowyText.medium(
action.name,
fontSize: 14,
),
// _buildHeaderWidget(context),
const Space(0, 10),
_buildResultWidget(context),
const Space(0, 10),
@@ -238,27 +242,6 @@ class _SmartEditInputWidgetState extends State<SmartEditInputWidget> {
);
}
Widget _buildHeaderWidget(BuildContext context) {
return Row(
children: [
FlowyText.medium(
'${LocaleKeys.document_plugins_openAI.tr()}: ${action.name}',
fontSize: 14,
),
const Spacer(),
FlowyButton(
useIntrinsicWidth: true,
text: FlowyText.regular(
LocaleKeys.document_plugins_autoGeneratorLearnMore.tr(),
),
onTap: () async {
await openLearnMorePage();
},
),
],
);
}
Widget _buildResultWidget(BuildContext context) {
final loadingWidget = Padding(
padding: const EdgeInsets.symmetric(horizontal: 4.0),
@@ -423,45 +406,33 @@ class _SmartEditInputWidgetState extends State<SmartEditInputWidget> {
result = "";
});
}
final openAIRepository = await getIt.getAsync<OpenAIRepository>();
var lines = content.split('\n\n');
if (action == SmartEditAction.summarize) {
lines = [lines.join('\n')];
}
for (var i = 0; i < lines.length; i++) {
final element = lines[i];
await openAIRepository.getStreamedCompletions(
useAction: true,
prompt: action.prompt(element),
onStart: () async {
setState(() {
loading = false;
});
},
onProcess: (response) async {
setState(() {
if (response.choices.first.text != '\n') {
result += response.choices.first.text;
}
});
},
onEnd: () async {
setState(() {
if (i != lines.length - 1) {
result += '\n';
}
});
},
onError: (error) async {
showSnackBarMessage(
context,
error.message,
showCancel: true,
);
await _onExit();
},
);
}
final aiResitory = await getIt.getAsync<AIRepository>();
await aiResitory.streamCompletion(
text: content,
completionType: completionTypeFromInt(action),
onStart: () async {
setState(() {
loading = false;
});
},
onProcess: (text) async {
setState(() {
result += text;
});
},
onEnd: () async {
setState(() {
result += '\n';
});
},
onError: (error) async {
showSnackBarMessage(
context,
error.message,
showCancel: true,
);
await _onExit();
},
);
}
}
@@ -4,6 +4,7 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.da
import 'package:appflowy/user/application/user_service.dart';
import 'package:appflowy/workspace/presentation/home/toast.dart';
import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart';
@@ -32,7 +33,7 @@ class SmartEditActionList extends StatefulWidget {
}
class _SmartEditActionListState extends State<SmartEditActionList> {
bool isOpenAIEnabled = false;
bool isAIEnabled = false;
@override
void initState() {
@@ -40,8 +41,9 @@ class _SmartEditActionListState extends State<SmartEditActionList> {
UserBackendService.getCurrentUserProfile().then((value) {
setState(() {
isOpenAIEnabled = value.fold(
(s) => s.openaiKey.isNotEmpty,
isAIEnabled = value.fold(
(userProfile) =>
userProfile.authenticator == AuthenticatorPB.AppFlowyCloud,
(_) => false,
);
});
@@ -60,9 +62,9 @@ class _SmartEditActionListState extends State<SmartEditActionList> {
keepEditorFocusNotifier.increase();
return FlowyIconButton(
hoverColor: Colors.transparent,
tooltipText: isOpenAIEnabled
tooltipText: isAIEnabled
? LocaleKeys.document_plugins_smartEdit.tr()
: LocaleKeys.document_plugins_smartEditDisabled.tr(),
: LocaleKeys.document_plugins_appflowyAIEditDisabled.tr(),
preferBelow: false,
icon: const Icon(
Icons.lightbulb_outline,
@@ -70,12 +72,12 @@ class _SmartEditActionListState extends State<SmartEditActionList> {
color: Colors.white,
),
onPressed: () {
if (isOpenAIEnabled) {
if (isAIEnabled) {
controller.show();
} else {
showSnackBarMessage(
context,
LocaleKeys.document_plugins_smartEditDisabled.tr(),
LocaleKeys.document_plugins_appflowyAIEditDisabled.tr(),
showCancel: true,
);
}
@@ -31,8 +31,10 @@ import 'package:image_picker/image_picker.dart';
class PageStyleCoverImage extends StatelessWidget {
PageStyleCoverImage({
super.key,
required this.documentId,
});
final String documentId;
late final ImagePicker _imagePicker = ImagePicker();
@override
@@ -230,7 +232,7 @@ class PageStyleCoverImage extends StatelessWidget {
type = PageStyleCoverImageType.localImage;
} else {
// else we should save the image to cloud storage
(result, _) = await saveImageToCloudStorage(path);
(result, _) = await saveImageToCloudStorage(path, documentId);
type = PageStyleCoverImageType.customImage;
}
if (!context.mounted) {
@@ -30,7 +30,7 @@ class PageStyleBottomSheet extends StatelessWidget {
fontSize: 14.0,
),
const VSpace(8.0),
PageStyleCoverImage(),
PageStyleCoverImage(documentId: view.id),
const VSpace(20.0),
// layout: font size, line height and font family.
FlowyText(
@@ -0,0 +1,130 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/application/document_share_bloc.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/util/string_extension.dart';
import 'package:appflowy/util/theme_extension.dart';
import 'package:appflowy/workspace/application/export/document_exporter.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/file_picker/file_picker_service.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class ExportTab extends StatelessWidget {
const ExportTab({
super.key,
});
@override
Widget build(BuildContext context) {
return Column(
children: [
const VSpace(10),
_ExportButton(
title: LocaleKeys.shareAction_html.tr(),
svg: FlowySvgs.export_html_s,
onTap: () => _exportHTML(context),
),
const VSpace(10),
_ExportButton(
title: LocaleKeys.shareAction_markdown.tr(),
svg: FlowySvgs.export_markdown_s,
onTap: () => _exportMarkdown(context),
),
const VSpace(10),
_ExportButton(
title: LocaleKeys.shareAction_clipboard.tr(),
svg: FlowySvgs.duplicate_s,
onTap: () => _exportToClipboard(context),
),
],
);
}
Future<void> _exportHTML(BuildContext context) async {
final viewName = context.read<DocumentShareBloc>().state.viewName;
final exportPath = await getIt<FilePickerService>().saveFile(
dialogTitle: '',
fileName: '${viewName.toFileName()}.html',
);
if (context.mounted && exportPath != null) {
context.read<DocumentShareBloc>().add(
DocumentShareEvent.share(
DocumentShareType.html,
exportPath,
),
);
}
}
Future<void> _exportMarkdown(BuildContext context) async {
final viewName = context.read<DocumentShareBloc>().state.viewName;
final exportPath = await getIt<FilePickerService>().saveFile(
dialogTitle: '',
fileName: '${viewName.toFileName()}.md',
);
if (context.mounted && exportPath != null) {
context.read<DocumentShareBloc>().add(
DocumentShareEvent.share(
DocumentShareType.markdown,
exportPath,
),
);
}
}
Future<void> _exportToClipboard(BuildContext context) async {
final documentExporter =
DocumentExporter(context.read<DocumentShareBloc>().view);
final result = await documentExporter.export(DocumentExportType.markdown);
result.fold(
(markdown) {
getIt<ClipboardService>().setData(
ClipboardServiceData(plainText: markdown),
);
showToastNotification(
context,
message: LocaleKeys.grid_url_copiedNotification.tr(),
);
},
(error) => showToastNotification(context, message: error.msg),
);
}
}
class _ExportButton extends StatelessWidget {
const _ExportButton({
required this.title,
required this.svg,
required this.onTap,
});
final String title;
final FlowySvgData svg;
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
final color = Theme.of(context).isLightMode
? const Color(0x1E14171B)
: Colors.white.withOpacity(0.1);
final radius = BorderRadius.circular(10.0);
return FlowyButton(
margin: const EdgeInsets.symmetric(vertical: 10, horizontal: 14),
iconPadding: 12,
decoration: BoxDecoration(
border: Border.all(
color: color,
),
borderRadius: radius,
),
radius: radius,
text: FlowyText(title),
leftIcon: FlowySvg(svg),
onTap: onTap,
);
}
}
@@ -0,0 +1,11 @@
import 'package:appflowy/util/theme_extension.dart';
import 'package:flutter/material.dart';
class ShareMenuColors {
static Color borderColor(BuildContext context) {
final borderColor = Theme.of(context).isLightMode
? const Color(0x1E14171B)
: Colors.white.withOpacity(0.1);
return borderColor;
}
}
@@ -0,0 +1,19 @@
String replaceInvalidChars(String input) {
final RegExp invalidCharsRegex = RegExp('[^a-zA-Z0-9-]');
return input.replaceAll(invalidCharsRegex, '-');
}
Future<String> generateNameSpace() async {
return '';
}
// The backend limits the publish name to a maximum of 120 characters.
// If the combined length of the ID and the name exceeds 120 characters,
// we will truncate the name to ensure the final result is within the limit.
// The name should only contain alphanumeric characters and hyphens.
Future<String> generatePublishName(String id, String name) async {
if (name.length >= 120 - id.length) {
name = name.substring(0, 120 - id.length);
}
return replaceInvalidChars('$name-$id');
}
@@ -1,10 +1,15 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/application/document_share_bloc.dart';
import 'package:appflowy/workspace/presentation/home/toast.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';
import 'package:appflowy/plugins/document/presentation/share/pubish_color_extension.dart';
import 'package:appflowy/plugins/document/presentation/share/publish_name_generator.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/style_widget/hover.dart';
import 'package:flowy_infra_ui/widget/rounded_button.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@@ -16,13 +21,7 @@ class PublishTab extends StatelessWidget {
Widget build(BuildContext context) {
return BlocConsumer<DocumentShareBloc, DocumentShareState>(
listener: (context, state) {
if (state.publishResult != null) {
state.publishResult!.fold(
(value) => showSnackBarMessage(context, 'Published successfully'),
(error) =>
showSnackBarMessage(context, 'Failed to publish: $error'),
);
}
_showToast(context, state);
},
builder: (context, state) {
return state.isPublished
@@ -36,13 +35,49 @@ class PublishTab extends StatelessWidget {
},
)
: _UnPublishWidget(
onPublish: () => context
.read<DocumentShareBloc>()
.add(const DocumentShareEvent.publish('')),
onPublish: () async {
final id = context.read<DocumentShareBloc>().view.id;
final publishName = await generatePublishName(
id,
state.viewName,
);
if (context.mounted) {
context.read<DocumentShareBloc>().add(
DocumentShareEvent.publish('', publishName),
);
}
},
);
},
);
}
void _showToast(BuildContext context, DocumentShareState state) {
if (state.publishResult != null) {
state.publishResult!.fold(
(value) => showToastNotification(
context,
message: LocaleKeys.publish_publishSuccessfully.tr(),
),
(error) => showToastNotification(
context,
message: '${LocaleKeys.publish_publishFailed.tr()}: ${error.code}',
),
);
} else if (state.unpublishResult != null) {
state.unpublishResult!.fold(
(value) => showToastNotification(
context,
message: LocaleKeys.publish_unpublishSuccessfully.tr(),
),
(error) => showToastNotification(
context,
message: LocaleKeys.publish_unpublishFailed.tr(),
description: error.msg,
),
);
}
}
}
class _PublishedWidget extends StatefulWidget {
@@ -86,61 +121,63 @@ class _PublishedWidgetState extends State<_PublishedWidget> {
const VSpace(16),
_PublishUrl(
controller: controller,
onCopy: (url) {},
onCopy: (url) {
getIt<ClipboardService>().setData(
ClipboardServiceData(plainText: url),
);
showToastNotification(
context,
message: LocaleKeys.grid_url_copy.tr(),
);
},
onSubmitted: (url) {},
),
const VSpace(16),
Row(
mainAxisSize: MainAxisSize.min,
children: [
_buildButton(
context,
name: LocaleKeys.shareAction_unPublish.tr(),
borderColor: const Color(0x1E14171B),
onTap: widget.onUnPublish,
),
_buildUnpublishButton(),
const Spacer(),
_buildButton(
context,
name: LocaleKeys.shareAction_visitSite.tr(),
backgroundColor: Theme.of(context).colorScheme.primary,
textColor: Colors.white,
onTap: () {
safeLaunchUrl(controller.text);
},
),
_buildVisitSiteButton(),
],
),
],
);
}
Widget _buildButton(
BuildContext context, {
required String name,
Color? backgroundColor,
Color borderColor = Colors.transparent,
Color? textColor,
required VoidCallback onTap,
}) {
Widget _buildUnpublishButton() {
return SizedBox(
height: 36,
width: 189,
width: 184,
child: FlowyButton(
decoration: BoxDecoration(
color: backgroundColor,
borderRadius: BorderRadius.circular(10),
border: Border.all(color: borderColor),
border: Border.all(color: ShareMenuColors.borderColor(context)),
),
radius: BorderRadius.circular(10),
text: FlowyText.regular(
name,
LocaleKeys.shareAction_unPublish.tr(),
textAlign: TextAlign.center,
color: textColor,
),
onTap: onTap,
onTap: widget.onUnPublish,
),
);
}
Widget _buildVisitSiteButton() {
return RoundedTextButton(
onPressed: () {
safeLaunchUrl(controller.text);
},
title: LocaleKeys.shareAction_visitSite.tr(),
width: 184,
height: 36,
borderRadius: const BorderRadius.all(Radius.circular(10)),
fillColor: Theme.of(context).colorScheme.primary,
textColor: Theme.of(context).colorScheme.onPrimary,
);
}
}
class _UnPublishWidget extends StatelessWidget {
@@ -216,16 +253,17 @@ class _PublishUrl extends StatelessWidget {
return SizedBox(
height: 36,
child: FlowyTextField(
readOnly: true,
autoFocus: false,
controller: controller,
suffixIcon: _buildCopyLinkIcon(),
enableBorderColor: ShareMenuColors.borderColor(context),
suffixIcon: _buildCopyLinkIcon(context),
),
);
}
Widget _buildCopyLinkIcon() {
return MouseRegion(
cursor: SystemMouseCursors.click,
Widget _buildCopyLinkIcon(BuildContext context) {
return FlowyHover(
child: GestureDetector(
onTap: () => onCopy(controller.text),
child: Container(
@@ -43,6 +43,10 @@ class DocumentShareButton extends StatelessWidget {
},
child: BlocBuilder<DocumentShareBloc, DocumentShareState>(
builder: (context, state) {
final tabs = [
if (state.enablePublish) ShareMenuTab.publish,
ShareMenuTab.exportAs,
];
final shareBloc = context.read<DocumentShareBloc>();
return SizedBox(
height: 32.0,
@@ -52,18 +56,14 @@ class DocumentShareButton extends StatelessWidget {
constraints: const BoxConstraints(
maxWidth: 422,
),
margin: const EdgeInsets.all(16),
offset: const Offset(0, 8),
popupBuilder: (context) => BlocProvider.value(
value: shareBloc,
child: const ShareMenu(),
),
child: RoundedTextButton(
title: LocaleKeys.shareAction_buttonText.tr(),
padding: const EdgeInsets.symmetric(horizontal: 12.0),
fontSize: 14.0,
textColor: Theme.of(context).colorScheme.onPrimary,
child: ShareMenu(
tabs: tabs,
),
),
child: const InnerDocumentShareButton(),
),
),
);
@@ -98,6 +98,24 @@ class DocumentShareButton extends StatelessWidget {
}
}
class InnerDocumentShareButton extends StatelessWidget {
const InnerDocumentShareButton({super.key});
@override
Widget build(BuildContext context) {
return RoundedTextButton(
title: LocaleKeys.shareAction_buttonText.tr(),
padding: const EdgeInsets.symmetric(horizontal: 14.0),
fontSize: 14.0,
fontWeight: FontWeight.w500,
borderRadius: const BorderRadius.all(
Radius.circular(10.0),
),
textColor: Theme.of(context).colorScheme.onPrimary,
);
}
}
class ShareActionList extends StatefulWidget {
const ShareActionList({
super.key,
@@ -1,8 +1,12 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:custom_sliding_segmented_control/custom_sliding_segmented_control.dart';
import 'package:appflowy/mobile/presentation/home/tab/_round_underline_tab_indicator.dart';
import 'package:appflowy/plugins/document/application/document_share_bloc.dart';
import 'package:appflowy/plugins/document/presentation/share/export_tab.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'publish_tab.dart';
@@ -11,79 +15,103 @@ enum ShareMenuTab {
publish,
exportAs;
static List<ShareMenuTab> supportedTabs = [
ShareMenuTab.share,
ShareMenuTab.publish,
ShareMenuTab.exportAs,
];
String get i18n {
switch (this) {
case ShareMenuTab.share:
return 'Share';
return LocaleKeys.shareAction_shareTab.tr();
case ShareMenuTab.publish:
return LocaleKeys.shareAction_publish;
return LocaleKeys.shareAction_publishTab.tr();
case ShareMenuTab.exportAs:
return 'Export as';
return LocaleKeys.shareAction_exportAsTab.tr();
}
}
}
class ShareMenu extends StatefulWidget {
const ShareMenu({super.key});
const ShareMenu({
super.key,
required this.tabs,
});
final List<ShareMenuTab> tabs;
@override
State<ShareMenu> createState() => _ShareMenuState();
}
class _ShareMenuState extends State<ShareMenu> {
ShareMenuTab selectedTab = ShareMenuTab.publish;
class _ShareMenuState extends State<ShareMenu>
with SingleTickerProviderStateMixin {
late ShareMenuTab selectedTab = widget.tabs.first;
late final tabController = TabController(
length: widget.tabs.length,
vsync: this,
initialIndex: widget.tabs.indexOf(selectedTab),
);
@override
Widget build(BuildContext context) {
final children = {
for (final tab in ShareMenuTab.supportedTabs)
tab: _Segment(
title: tab.i18n.tr(),
isSelected: selectedTab == tab,
),
};
if (widget.tabs.isEmpty) {
return const SizedBox.shrink();
}
return Column(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(
height: 34,
child: CustomSlidingSegmentedControl<ShareMenuTab>(
initialValue: selectedTab,
curve: Curves.linear,
padding: 0,
fixedWidth: 128,
innerPadding: const EdgeInsets.all(3.0),
children: children,
decoration: BoxDecoration(
color: const Color(0xFFEEF0F3),
borderRadius: BorderRadius.circular(8),
),
thumbDecoration: BoxDecoration(
color: Colors.white,
boxShadow: const [
BoxShadow(
color: Color(0x141F2225),
blurRadius: 8,
offset: Offset(0, 2),
),
],
borderRadius: BorderRadius.circular(8),
),
onValueChanged: (v) {
setState(() {
selectedTab = v;
});
},
const VSpace(10),
Container(
alignment: Alignment.centerLeft,
height: 30,
child: _buildTabBar(context),
),
Divider(
color: Theme.of(context).dividerColor,
height: 1,
thickness: 1,
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 14.0),
child: _buildTab(context),
),
const VSpace(20),
],
);
}
@override
void dispose() {
tabController.dispose();
super.dispose();
}
Widget _buildTabBar(BuildContext context) {
final children = [
for (final tab in widget.tabs)
Padding(
padding: const EdgeInsets.only(bottom: 10),
child: _Segment(
tab: tab,
isSelected: selectedTab == tab,
),
),
_buildTab(context),
],
];
return TabBar(
indicatorSize: TabBarIndicatorSize.label,
indicator: RoundUnderlineTabIndicator(
width: 68.0,
borderSide: BorderSide(
color: Theme.of(context).colorScheme.primary,
width: 3,
),
insets: const EdgeInsets.only(bottom: -2),
),
isScrollable: true,
controller: tabController,
tabs: children,
onTap: (index) {
setState(() {
selectedTab = widget.tabs[index];
});
},
);
}
@@ -91,31 +119,68 @@ class _ShareMenuState extends State<ShareMenu> {
switch (selectedTab) {
case ShareMenuTab.publish:
return const PublishTab();
case ShareMenuTab.exportAs:
return const ExportTab();
default:
return const Center(
child: FlowyText('coming soon'),
child: FlowyText('🏡 under construction'),
);
}
}
}
class _Segment extends StatelessWidget {
class _Segment extends StatefulWidget {
const _Segment({
required this.title,
required this.tab,
required this.isSelected,
});
final String title;
final bool isSelected;
final ShareMenuTab tab;
@override
State<_Segment> createState() => _SegmentState();
}
class _SegmentState extends State<_Segment> {
bool isHovered = false;
@override
Widget build(BuildContext context) {
final textColor = isSelected ? null : Theme.of(context).hintColor;
return FlowyText(
title,
textAlign: TextAlign.center,
color: textColor,
Color? textColor = Theme.of(context).hintColor;
if (isHovered) {
textColor = const Color(0xFF00BCF0);
} else if (widget.isSelected) {
textColor = null;
}
Widget child = MouseRegion(
onEnter: (_) => setState(() => isHovered = true),
onExit: (_) => setState(() => isHovered = false),
child: FlowyText(
widget.tab.i18n,
textAlign: TextAlign.center,
color: textColor,
),
);
if (widget.tab == ShareMenuTab.publish) {
final isPublished = context.watch<DocumentShareBloc>().state.isPublished;
// show checkmark icon if published
if (isPublished) {
child = Row(
children: [
const FlowySvg(
FlowySvgs.published_checkmark_s,
blendMode: null,
),
const HSpace(6),
child,
],
);
}
}
return child;
}
}
@@ -3,13 +3,14 @@ import 'package:appflowy/core/network_monitor.dart';
import 'package:appflowy/env/cloud_env.dart';
import 'package:appflowy/plugins/document/application/prelude.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/openai_client.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/ai_client.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/stability_ai/stability_ai_client.dart';
import 'package:appflowy/plugins/trash/application/prelude.dart';
import 'package:appflowy/shared/appflowy_cache_manager.dart';
import 'package:appflowy/shared/custom_image_cache_manager.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/startup/tasks/appflowy_cloud_task.dart';
import 'package:appflowy/user/application/ai_service.dart';
import 'package:appflowy/user/application/auth/af_cloud_auth_service.dart';
import 'package:appflowy/user/application/auth/auth_service.dart';
import 'package:appflowy/user/application/auth/supabase_auth_service.dart';
@@ -85,15 +86,12 @@ void _resolveCommonService(
() => mode.isTest ? MockApplicationDataStorage() : ApplicationDataStorage(),
);
getIt.registerFactoryAsync<OpenAIRepository>(
getIt.registerFactoryAsync<AIRepository>(
() async {
final result = await UserBackendService.getCurrentUserProfile();
return result.fold(
(s) {
return HttpOpenAIRepository(
client: http.Client(),
apiKey: s.openaiKey,
);
return AppFlowyAIService();
},
(e) {
throw Exception('Failed to get user profile: ${e.msg}');
@@ -83,4 +83,19 @@ class WindowSizeManager {
'${scaleFactor.clamp(minScaleFactor, maxScaleFactor)}',
);
}
/// Set the window maximized status
Future<void> setWindowMaximized(bool isMaximized) async {
await getIt<KeyValueStorage>()
.set(KVKeys.windowMaximized, isMaximized.toString());
}
/// Get the window maximized status
Future<bool> getWindowMaximized() async {
return await getIt<KeyValueStorage>().getWithFormat<bool>(
KVKeys.windowMaximized,
(v) => bool.tryParse(v) ?? false,
) ??
false;
}
}
@@ -13,12 +13,9 @@ import 'package:scaled_app/scaled_app.dart';
import 'package:window_manager/window_manager.dart';
class InitAppWindowTask extends LaunchTask with WindowListener {
InitAppWindowTask({
this.title = 'AppFlowy',
});
InitAppWindowTask({this.title = 'AppFlowy'});
final String title;
final windowSizeManager = WindowSizeManager();
@override
@@ -48,7 +45,7 @@ class InitAppWindowTask extends LaunchTask with WindowListener {
final position = await windowSizeManager.getPosition();
if (PlatformExtension.isWindows) {
doWhenWindowReady(() {
doWhenWindowReady(() async {
appWindow.minSize = windowOptions.minimumSize;
appWindow.maxSize = windowOptions.maximumSize;
appWindow.size = windowSize;
@@ -58,6 +55,13 @@ class InitAppWindowTask extends LaunchTask with WindowListener {
}
appWindow.show();
/// on Windows we maximize the window if it was previously closed
/// from a maximized state.
final isMaximized = await windowSizeManager.getWindowMaximized();
if (isMaximized) {
appWindow.maximize();
}
});
} else {
await windowManager.waitUntilReadyToShow(windowOptions, () async {
@@ -78,16 +82,40 @@ class InitAppWindowTask extends LaunchTask with WindowListener {
}
@override
Future<void> onWindowResize() async {
super.onWindowResize();
final currentWindowSize = await windowManager.getSize();
return windowSizeManager.setSize(currentWindowSize);
Future<void> onWindowMaximize() async {
super.onWindowMaximize();
await windowSizeManager.setWindowMaximized(true);
await windowSizeManager.setPosition(Offset.zero);
}
@override
void onWindowMaximize() async {
super.onWindowMaximize();
Future<void> onWindowUnmaximize() async {
super.onWindowUnmaximize();
await windowSizeManager.setWindowMaximized(false);
final position = await windowManager.getPosition();
return windowSizeManager.setPosition(position);
}
@override
void onWindowEnterFullScreen() async {
super.onWindowEnterFullScreen();
await windowSizeManager.setWindowMaximized(true);
await windowSizeManager.setPosition(Offset.zero);
}
@override
Future<void> onWindowLeaveFullScreen() async {
super.onWindowLeaveFullScreen();
await windowSizeManager.setWindowMaximized(false);
final position = await windowManager.getPosition();
return windowSizeManager.setPosition(position);
}
@override
Future<void> onWindowResize() async {
super.onWindowResize();
final currentWindowSize = await windowManager.getSize();
return windowSizeManager.setSize(currentWindowSize);
@@ -0,0 +1,122 @@
import 'dart:async';
import 'dart:ffi';
import 'dart:isolate';
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/ai_client.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/error.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/text_completion.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/widgets/smart_edit_action.dart';
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/protobuf/flowy-chat/entities.pb.dart';
import 'package:appflowy_result/appflowy_result.dart';
import 'package:fixnum/fixnum.dart' as fixnum;
class AppFlowyAIService implements AIRepository {
@override
Future<FlowyResult<List<String>, AIError>> generateImage({
required String prompt,
int n = 1,
}) {
throw UnimplementedError();
}
@override
Future<void> getStreamedCompletions({
required String prompt,
required Future<void> Function() onStart,
required Future<void> Function(TextCompletionResponse response) onProcess,
required Future<void> Function() onEnd,
required void Function(AIError error) onError,
String? suffix,
int maxTokens = 2048,
double temperature = 0.3,
bool useAction = false,
}) {
throw UnimplementedError();
}
@override
Future<CompletionStream> streamCompletion({
required String text,
required CompletionTypePB completionType,
required Future<void> Function() onStart,
required Future<void> Function(String text) onProcess,
required Future<void> Function() onEnd,
required void Function(AIError error) onError,
}) async {
final stream = CompletionStream(
onStart,
onProcess,
onEnd,
onError,
);
final payload = CompleteTextPB(
text: text,
completionType: completionType,
streamPort: fixnum.Int64(stream.nativePort),
);
// ignore: unawaited_futures
ChatEventCompleteText(payload).send();
return stream;
}
}
CompletionTypePB completionTypeFromInt(SmartEditAction action) {
switch (action) {
case SmartEditAction.summarize:
return CompletionTypePB.MakeShorter;
case SmartEditAction.fixSpelling:
return CompletionTypePB.SpellingAndGrammar;
case SmartEditAction.improveWriting:
return CompletionTypePB.ImproveWriting;
case SmartEditAction.makeItLonger:
return CompletionTypePB.MakeLonger;
}
}
class CompletionStream {
CompletionStream(
Future<void> Function() onStart,
Future<void> Function(String text) onProcess,
Future<void> Function() onEnd,
void Function(AIError error) onError,
) {
_port.handler = _controller.add;
_subscription = _controller.stream.listen(
(event) async {
if (event.startsWith("start:")) {
await onStart();
}
if (event.startsWith("data:")) {
await onProcess(event.substring(5));
}
if (event.startsWith("finish:")) {
await onEnd();
}
if (event.startsWith("error:")) {
onError(AIError(message: event.substring(6)));
}
},
);
}
final RawReceivePort _port = RawReceivePort();
final StreamController<String> _controller = StreamController.broadcast();
late StreamSubscription<String> _subscription;
int get nativePort => _port.sendPort.nativePort;
Future<void> dispose() async {
await _controller.close();
await _subscription.cancel();
_port.close();
}
StreamSubscription<String> listen(
void Function(String event)? onData,
) {
return _controller.stream.listen(onData);
}
}
@@ -1,6 +1,7 @@
import 'dart:async';
import 'dart:typed_data';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:flutter/foundation.dart';
import 'package:appflowy/core/notification/folder_notification.dart';
@@ -11,7 +12,6 @@ import 'package:appflowy_backend/protobuf/flowy-folder/workspace.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-notification/protobuf.dart';
import 'package:appflowy_backend/protobuf/flowy-user/notification.pb.dart'
as user;
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
import 'package:appflowy_backend/rust_stream.dart';
import 'package:appflowy_result/appflowy_result.dart';
import 'package:flowy_infra/notifier.dart';
@@ -23,6 +23,9 @@ typedef DidUpdateUserWorkspacesCallback = void Function(
RepeatedUserWorkspacePB workspaces,
);
typedef UserProfileNotifyValue = FlowyResult<UserProfilePB, FlowyError>;
typedef DidUpdateUserWorkspaceSetting = void Function(
UseAISettingPB settings,
);
class UserListener {
UserListener({
@@ -37,28 +40,26 @@ class UserListener {
/// Update notification about _all_ of the users workspaces
///
DidUpdateUserWorkspacesCallback? didUpdateUserWorkspaces;
DidUpdateUserWorkspacesCallback? onUserWorkspaceListUpdated;
/// Update notification about _one_ workspace
///
DidUpdateUserWorkspaceCallback? didUpdateUserWorkspace;
DidUpdateUserWorkspaceCallback? onUserWorkspaceUpdated;
DidUpdateUserWorkspaceSetting? onUserWorkspaceSettingUpdated;
void start({
void Function(UserProfileNotifyValue)? onProfileUpdated,
void Function(RepeatedUserWorkspacePB)? didUpdateUserWorkspaces,
void Function(UserWorkspacePB)? didUpdateUserWorkspace,
DidUpdateUserWorkspacesCallback? onUserWorkspaceListUpdated,
void Function(UserWorkspacePB)? onUserWorkspaceUpdated,
DidUpdateUserWorkspaceSetting? onUserWorkspaceSettingUpdated,
}) {
if (onProfileUpdated != null) {
_profileNotifier?.addPublishListener(onProfileUpdated);
}
if (didUpdateUserWorkspaces != null) {
this.didUpdateUserWorkspaces = didUpdateUserWorkspaces;
}
if (didUpdateUserWorkspace != null) {
this.didUpdateUserWorkspace = didUpdateUserWorkspace;
}
this.onUserWorkspaceListUpdated = onUserWorkspaceListUpdated;
this.onUserWorkspaceUpdated = onUserWorkspaceUpdated;
this.onUserWorkspaceSettingUpdated = onUserWorkspaceSettingUpdated;
_userParser = UserNotificationParser(
id: _userProfile.id.toString(),
@@ -92,13 +93,18 @@ class UserListener {
result.map(
(r) {
final value = RepeatedUserWorkspacePB.fromBuffer(r);
didUpdateUserWorkspaces?.call(value);
onUserWorkspaceListUpdated?.call(value);
},
);
break;
case user.UserNotification.DidUpdateUserWorkspace:
result.map(
(r) => didUpdateUserWorkspace?.call(UserWorkspacePB.fromBuffer(r)),
(r) => onUserWorkspaceUpdated?.call(UserWorkspacePB.fromBuffer(r)),
);
case user.UserNotification.DidUpdateAISetting:
result.map(
(r) =>
onUserWorkspaceSettingUpdated?.call(UseAISettingPB.fromBuffer(r)),
);
break;
default:
@@ -110,8 +116,8 @@ class UserListener {
typedef WorkspaceSettingNotifyValue
= FlowyResult<WorkspaceSettingPB, FlowyError>;
class UserWorkspaceListener {
UserWorkspaceListener();
class FolderListener {
FolderListener();
final PublishNotifier<WorkspaceSettingNotifyValue> _settingChangedNotifier =
PublishNotifier();
@@ -109,7 +109,7 @@ class UserBackendService implements IUserBackendService {
return UserEventOpenWorkspace(payload).send();
}
Future<FlowyResult<WorkspacePB, FlowyError>> getCurrentWorkspace() {
static Future<FlowyResult<WorkspacePB, FlowyError>> getCurrentWorkspace() {
return FolderEventReadCurrentWorkspace().send().then((result) {
return result.fold(
(workspace) => FlowyResult.success(workspace),
@@ -7,6 +7,7 @@ import 'package:appflowy/plugins/trash/application/trash_service.dart';
import 'package:appflowy/workspace/application/command_palette/search_listener.dart';
import 'package:appflowy/workspace/application/command_palette/search_service.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/trash.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-search/notification.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-search/result.pb.dart';
import 'package:bloc/bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
@@ -40,6 +41,7 @@ class CommandPaletteBloc
Future<void> close() {
_trashListener.close();
_searchListener.stop();
_debounceOnChanged?.cancel();
return super.close();
}
@@ -75,13 +77,17 @@ class CommandPaletteBloc
emit(state.copyWith(query: null, isLoading: false, results: []));
}
},
resultsChanged: (results, max) {
resultsChanged: (results) {
if (state.query != _oldQuery) {
emit(state.copyWith(results: []));
emit(state.copyWith(results: [], isLoading: true));
_oldQuery = state.query;
_messagesReceived = 0;
}
if (state.query != results.query) {
return;
}
_messagesReceived++;
final searchResults = _filterDuplicates(results.items);
@@ -90,7 +96,7 @@ class CommandPaletteBloc
emit(
state.copyWith(
results: searchResults,
isLoading: _messagesReceived != max,
isLoading: _messagesReceived != results.sends.toInt(),
),
);
},
@@ -155,7 +161,7 @@ class CommandPaletteBloc
void _performSearch(String value) =>
add(CommandPaletteEvent.performSearch(search: value));
void _onResultsChanged(RepeatedSearchResultPB results) =>
void _onResultsChanged(SearchResultNotificationPB results) =>
add(CommandPaletteEvent.resultsChanged(results: results));
}
@@ -168,8 +174,7 @@ class CommandPaletteEvent with _$CommandPaletteEvent {
_PerformSearch;
const factory CommandPaletteEvent.resultsChanged({
required RepeatedSearchResultPB results,
@Default(1) int max,
required SearchResultNotificationPB results,
}) = _ResultsChanged;
const factory CommandPaletteEvent.trashChanged({
@@ -4,8 +4,7 @@ import 'dart:typed_data';
import 'package:appflowy/core/notification/search_notification.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-search/notification.pbenum.dart';
import 'package:appflowy_backend/protobuf/flowy-search/result.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-search/notification.pb.dart';
import 'package:appflowy_result/appflowy_result.dart';
import 'package:flowy_infra/notifier.dart';
@@ -23,14 +22,15 @@ class SearchListener {
///
final String? channel;
PublishNotifier<RepeatedSearchResultPB>? _updateNotifier = PublishNotifier();
PublishNotifier<RepeatedSearchResultPB>? _updateDidCloseNotifier =
PublishNotifier<SearchResultNotificationPB>? _updateNotifier =
PublishNotifier();
PublishNotifier<SearchResultNotificationPB>? _updateDidCloseNotifier =
PublishNotifier();
SearchNotificationListener? _listener;
void start({
void Function(RepeatedSearchResultPB)? onResultsChanged,
void Function(RepeatedSearchResultPB)? onResultsClosed,
void Function(SearchResultNotificationPB)? onResultsChanged,
void Function(SearchResultNotificationPB)? onResultsClosed,
}) {
if (onResultsChanged != null) {
_updateNotifier?.addPublishListener(onResultsChanged);
@@ -55,7 +55,7 @@ class SearchListener {
case SearchNotification.DidUpdateResults:
result.fold(
(payload) => _updateNotifier?.value =
RepeatedSearchResultPB.fromBuffer(payload),
SearchResultNotificationPB.fromBuffer(payload),
(err) => Log.error(err),
);
break;
@@ -5,16 +5,17 @@ import 'package:appflowy_backend/protobuf/flowy-folder/workspace.pb.dart'
show WorkspaceSettingPB;
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'home_bloc.freezed.dart';
class HomeBloc extends Bloc<HomeEvent, HomeState> {
HomeBloc(WorkspaceSettingPB workspaceSetting)
: _workspaceListener = UserWorkspaceListener(),
: _workspaceListener = FolderListener(),
super(HomeState.initial(workspaceSetting)) {
_dispatch(workspaceSetting);
}
final UserWorkspaceListener _workspaceListener;
final FolderListener _workspaceListener;
@override
Future<void> close() async {
@@ -15,7 +15,7 @@ class HomeSettingBloc extends Bloc<HomeSettingEvent, HomeSettingState> {
WorkspaceSettingPB workspaceSetting,
AppearanceSettingsCubit appearanceSettingsCubit,
double screenWidthPx,
) : _listener = UserWorkspaceListener(),
) : _listener = FolderListener(),
_appearanceSettingsCubit = appearanceSettingsCubit,
super(
HomeSettingState.initial(
@@ -27,7 +27,7 @@ class HomeSettingBloc extends Bloc<HomeSettingEvent, HomeSettingState> {
_dispatch();
}
final UserWorkspaceListener _listener;
final FolderListener _listener;
final AppearanceSettingsCubit _appearanceSettingsCubit;
@override
@@ -13,7 +13,7 @@ part 'menu_user_bloc.freezed.dart';
class MenuUserBloc extends Bloc<MenuUserEvent, MenuUserState> {
MenuUserBloc(this.userProfile)
: _userListener = UserListener(userProfile: userProfile),
_userWorkspaceListener = UserWorkspaceListener(),
_userWorkspaceListener = FolderListener(),
_userService = UserBackendService(userId: userProfile.id),
super(MenuUserState.initial(userProfile)) {
_dispatch();
@@ -21,7 +21,7 @@ class MenuUserBloc extends Bloc<MenuUserEvent, MenuUserState> {
final UserBackendService _userService;
final UserListener _userListener;
final UserWorkspaceListener _userWorkspaceListener;
final FolderListener _userWorkspaceListener;
final UserProfilePB userProfile;
@override
@@ -54,9 +54,11 @@ class SidebarSectionsBloc
_initial(userProfile, workspaceId);
final sectionViews = await _getSectionViews();
if (sectionViews != null) {
final containsSpace = _containsSpace(sectionViews);
emit(
state.copyWith(
section: sectionViews,
containsSpace: containsSpace,
),
);
}
@@ -65,9 +67,11 @@ class SidebarSectionsBloc
_reset(userProfile, workspaceId);
final sectionViews = await _getSectionViews();
if (sectionViews != null) {
final containsSpace = _containsSpace(sectionViews);
emit(
state.copyWith(
section: sectionViews,
containsSpace: containsSpace,
),
);
}
@@ -160,9 +164,11 @@ class SidebarSectionsBloc
_initial(userProfile, workspaceId);
final sectionViews = await _getSectionViews();
if (sectionViews != null) {
final containsSpace = _containsSpace(sectionViews);
emit(
state.copyWith(
section: sectionViews,
containsSpace: containsSpace,
),
);
// try to open the fist view in public section or private section
@@ -229,6 +235,11 @@ class SidebarSectionsBloc
}
}
bool _containsSpace(SidebarSection section) {
return section.publicViews.any((view) => view.isSpace) ||
section.privateViews.any((view) => view.isSpace);
}
void _initial(UserProfilePB userProfile, String workspaceId) {
_workspaceService = WorkspaceService(workspaceId: workspaceId);
@@ -292,6 +303,7 @@ class SidebarSectionsState with _$SidebarSectionsState {
required SidebarSection section,
@Default(null) ViewPB? lastCreatedRootView,
FlowyResult<void, FlowyError>? createRootViewResult,
@Default(true) bool containsSpace,
}) = _SidebarSectionsState;
factory SidebarSectionsState.initial() => const SidebarSectionsState(
@@ -0,0 +1,195 @@
import 'dart:io';
import 'package:appflowy/plugins/ai_chat/application/chat_bloc.dart';
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-chat/entities.pb.dart';
import 'package:bloc/bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:path/path.dart' as path;
import 'package:protobuf/protobuf.dart';
part 'setting_local_ai_bloc.freezed.dart';
class SettingsAILocalBloc
extends Bloc<SettingsAILocalEvent, SettingsAILocalState> {
SettingsAILocalBloc() : super(const SettingsAILocalState()) {
on<SettingsAILocalEvent>(_handleEvent);
}
/// Handles incoming events and dispatches them to the appropriate handler.
Future<void> _handleEvent(
SettingsAILocalEvent event,
Emitter<SettingsAILocalState> emit,
) async {
await event.when(
started: _handleStarted,
didUpdateAISetting: (settings) async {
_handleDidUpdateAISetting(settings, emit);
},
updateChatBin: (chatBinPath) async {
await _handleUpdatePath(
filePath: chatBinPath,
emit: emit,
stateUpdater: () => state.copyWith(
chatBinPath: chatBinPath.trim(),
chatBinPathError: null,
),
errorUpdater: (error) => state.copyWith(chatBinPathError: error),
);
},
updateChatModelPath: (chatModelPath) async {
await _handleUpdatePath(
filePath: chatModelPath,
emit: emit,
stateUpdater: () => state.copyWith(
chatModelPath: chatModelPath.trim(),
chatModelPathError: null,
),
errorUpdater: (error) => state.copyWith(chatModelPathError: error),
);
},
toggleLocalAI: () async {
emit(state.copyWith(localAIEnabled: !state.localAIEnabled));
},
saveSetting: () async {
_handleSaveSetting();
},
);
}
/// Handles the event to fetch local AI settings when the application starts.
Future<void> _handleStarted() async {
final result = await ChatEventGetLocalAISetting().send();
result.fold(
(setting) {
if (!isClosed) {
add(SettingsAILocalEvent.didUpdateAISetting(setting));
}
},
(err) => Log.error('Failed to get local AI setting: $err'),
);
}
/// Handles the event to update the AI settings in the state.
void _handleDidUpdateAISetting(
LocalLLMSettingPB settings,
Emitter<SettingsAILocalState> emit,
) {
final newState = state.copyWith(
aiSettings: settings,
chatBinPath: settings.chatBinPath,
chatModelPath: settings.chatModelPath,
localAIEnabled: settings.enabled,
loadingState: const LoadingState.finish(),
);
emit(newState.copyWith(saveButtonEnabled: _saveButtonEnabled(newState)));
}
/// Handles updating file paths (both chat binary and chat model paths).
Future<void> _handleUpdatePath({
required String filePath,
required Emitter<SettingsAILocalState> emit,
required SettingsAILocalState Function() stateUpdater,
required SettingsAILocalState Function(String) errorUpdater,
}) async {
filePath = filePath.trim();
if (filePath.isEmpty) {
emit(stateUpdater());
return;
}
final validationError = await _validatePath(filePath);
if (validationError != null) {
emit(errorUpdater(validationError));
return;
}
final newState = stateUpdater();
emit(newState.copyWith(saveButtonEnabled: _saveButtonEnabled(newState)));
}
/// Validates the provided file path.
Future<String?> _validatePath(String filePath) async {
if (!isAbsolutePath(filePath)) {
return "$filePath must be absolute";
}
if (!await pathExists(filePath)) {
return "$filePath does not exist";
}
return null;
}
/// Handles saving the updated settings.
void _handleSaveSetting() {
if (state.aiSettings == null) return;
state.aiSettings!.freeze();
final newSetting = state.aiSettings!.rebuild((value) {
value
..chatBinPath = state.chatBinPath ?? value.chatBinPath
..chatModelPath = state.chatModelPath ?? value.chatModelPath
..enabled = state.localAIEnabled;
});
ChatEventUpdateLocalAISetting(newSetting).send().then((result) {
result.fold(
(_) {
if (!isClosed) {
add(SettingsAILocalEvent.didUpdateAISetting(newSetting));
}
},
(err) => Log.error('Failed to update local AI setting: $err'),
);
});
}
/// Determines if the save button should be enabled based on the state.
bool _saveButtonEnabled(SettingsAILocalState newState) {
return newState.chatBinPathError == null &&
newState.chatModelPathError == null &&
newState.chatBinPath != null &&
newState.chatModelPath != null;
}
}
@freezed
class SettingsAILocalEvent with _$SettingsAILocalEvent {
const factory SettingsAILocalEvent.started() = _Started;
const factory SettingsAILocalEvent.didUpdateAISetting(
LocalLLMSettingPB settings,
) = _GetAISetting;
const factory SettingsAILocalEvent.updateChatBin(String chatBinPath) =
_UpdateChatBin;
const factory SettingsAILocalEvent.updateChatModelPath(String chatModelPath) =
_UpdateChatModelPath;
const factory SettingsAILocalEvent.toggleLocalAI() = _EnableLocalAI;
const factory SettingsAILocalEvent.saveSetting() = _SaveSetting;
}
@freezed
class SettingsAILocalState with _$SettingsAILocalState {
const factory SettingsAILocalState({
LocalLLMSettingPB? aiSettings,
String? chatBinPath,
String? chatBinPathError,
String? chatModelPath,
String? chatModelPathError,
@Default(false) bool localAIEnabled,
@Default(false) bool saveButtonEnabled,
@Default(LoadingState.loading()) LoadingState loadingState,
}) = _SettingsAILocalState;
}
/// Checks if a given file path is absolute.
bool isAbsolutePath(String filePath) {
return path.isAbsolute(filePath);
}
/// Checks if a given file or directory path exists.
Future<bool> pathExists(String filePath) async {
final file = File(filePath);
final directory = Directory(filePath);
return await file.exists() || await directory.exists();
}
@@ -0,0 +1,127 @@
import 'package:appflowy/user/application/user_listener.dart';
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:appflowy_result/appflowy_result.dart';
import 'package:bloc/bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'settings_ai_bloc.freezed.dart';
class SettingsAIBloc extends Bloc<SettingsAIEvent, SettingsAIState> {
SettingsAIBloc(this.userProfile)
: _userListener = UserListener(userProfile: userProfile),
super(SettingsAIState(userProfile: userProfile)) {
_dispatch();
}
final UserListener _userListener;
final UserProfilePB userProfile;
@override
Future<void> close() async {
await _userListener.stop();
return super.close();
}
void _dispatch() {
on<SettingsAIEvent>((event, emit) {
event.when(
started: () {
_userListener.start(
onProfileUpdated: _onProfileUpdated,
onUserWorkspaceSettingUpdated: (settings) {
if (!isClosed) {
add(SettingsAIEvent.didLoadAISetting(settings));
}
},
);
_loadUserWorkspaceSetting();
},
didReceiveUserProfile: (userProfile) {
emit(state.copyWith(userProfile: userProfile));
},
toggleAISearch: () {
_updateUserWorkspaceSetting(
disableSearchIndexing:
!(state.aiSettings?.disableSearchIndexing ?? false),
);
},
selectModel: (AIModelPB model) {
_updateUserWorkspaceSetting(model: model);
},
didLoadAISetting: (UseAISettingPB settings) {
emit(
state.copyWith(
aiSettings: settings,
enableSearchIndexing: !settings.disableSearchIndexing,
),
);
},
);
});
}
void _updateUserWorkspaceSetting({
bool? disableSearchIndexing,
AIModelPB? model,
}) {
final payload = UpdateUserWorkspaceSettingPB(
workspaceId: userProfile.workspaceId,
);
if (disableSearchIndexing != null) {
payload.disableSearchIndexing = disableSearchIndexing;
}
if (model != null) {
payload.aiModel = model;
}
UserEventUpdateWorkspaceSetting(payload).send();
}
void _onProfileUpdated(
FlowyResult<UserProfilePB, FlowyError> userProfileOrFailed,
) =>
userProfileOrFailed.fold(
(profile) => add(SettingsAIEvent.didReceiveUserProfile(profile)),
(err) => Log.error(err),
);
void _loadUserWorkspaceSetting() {
final payload = UserWorkspaceIdPB(workspaceId: userProfile.workspaceId);
UserEventGetWorkspaceSetting(payload).send().then((result) {
result.fold((settings) {
if (!isClosed) {
add(SettingsAIEvent.didLoadAISetting(settings));
}
}, (err) {
Log.error(err);
});
});
}
}
@freezed
class SettingsAIEvent with _$SettingsAIEvent {
const factory SettingsAIEvent.started() = _Started;
const factory SettingsAIEvent.didLoadAISetting(
UseAISettingPB settings,
) = _DidLoadWorkspaceSetting;
const factory SettingsAIEvent.toggleAISearch() = _toggleAISearch;
const factory SettingsAIEvent.selectModel(AIModelPB model) = _SelectAIModel;
const factory SettingsAIEvent.didReceiveUserProfile(
UserProfilePB newUserProfile,
) = _DidReceiveUserProfile;
}
@freezed
class SettingsAIState with _$SettingsAIState {
const factory SettingsAIState({
required UserProfilePB userProfile,
UseAISettingPB? aiSettings,
@Default(true) bool enableSearchIndexing,
}) = _SettingsAIState;
}
@@ -117,6 +117,10 @@ class SettingsPlanBloc extends Bloc<SettingsPlanEvent, SettingsPlanState> {
);
},
cancelSubscription: () async {
final newState = state
.mapOrNull(ready: (state) => state)
?.copyWith(downgradeProcessing: true);
emit(newState ?? state);
await _userService.cancelSubscription(workspaceId);
add(const SettingsPlanEvent.started());
},
@@ -174,5 +178,6 @@ class SettingsPlanState with _$SettingsPlanState {
required WorkspaceSubscriptionPB subscription,
required BillingPortalPB? billingPortal,
@Default(false) bool showSuccessDialog,
@Default(false) bool downgradeProcessing,
}) = _Ready;
}
@@ -1,8 +1,20 @@
import 'package:appflowy_backend/protobuf/flowy-user/workspace.pb.dart';
import 'package:intl/intl.dart';
final _storageNumberFormat = NumberFormat()
..maximumFractionDigits = 2
..minimumFractionDigits = 0;
extension PresentableUsage on WorkspaceUsagePB {
String get totalBlobInGb =>
(totalBlobBytesLimit.toInt() / 1024 / 1024 / 1024).round().toString();
/// We use [NumberFormat] to format the current blob in GB.
///
/// Where the [totalBlobBytes] is the total blob bytes in bytes.
/// And [NumberFormat.maximumFractionDigits] is set to 2.
/// And [NumberFormat.minimumFractionDigits] is set to 0.
///
String get currentBlobInGb =>
(totalBlobBytes.toInt() / 1024 / 1024 / 1024).round().toString();
_storageNumberFormat.format(totalBlobBytes.toInt() / 1024 / 1024 / 1024);
}
@@ -13,12 +13,13 @@ enum SettingsPage {
account,
workspace,
manageData,
shortcuts,
ai,
plan,
billing,
// OLD
notifications,
cloud,
shortcuts,
member,
featureFlags,
}
@@ -4,34 +4,29 @@ import 'package:appflowy_backend/protobuf/flowy-folder/import.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pbenum.dart';
import 'package:appflowy_result/appflowy_result.dart';
class ImportBackendService {
static Future<FlowyResult<void, FlowyError>> importData(
List<int> data,
String name,
String parentViewId,
ImportTypePB importType,
) async {
final payload = ImportPB.create()
..data = data
..parentViewId = parentViewId
..viewLayout = importType.toLayout()
..name = name
..importType = importType;
return FolderEventImportData(payload).send();
}
class ImportPayload {
ImportPayload({
required this.name,
required this.data,
required this.layout,
});
final String name;
final List<int> data;
final ViewLayoutPB layout;
}
extension on ImportTypePB {
ViewLayoutPB toLayout() {
switch (this) {
case ImportTypePB.HistoryDocument:
return ViewLayoutPB.Document;
case ImportTypePB.HistoryDatabase ||
ImportTypePB.CSV ||
ImportTypePB.RawDatabase:
return ViewLayoutPB.Grid;
default:
throw UnimplementedError('Unsupported import type $this');
}
class ImportBackendService {
static Future<FlowyResult<void, FlowyError>> importPages(
String parentViewId,
List<ImportValuePayloadPB> values,
) async {
final request = ImportPayloadPB(
parentViewId: parentViewId,
values: values,
syncAfterCreate: true,
);
return FolderEventImportData(request).send();
}
}
@@ -22,7 +22,7 @@ class WorkspaceSettingsBloc
try {
final currentWorkspace =
await _userService!.getCurrentWorkspace().getOrThrow();
await UserBackendService.getCurrentWorkspace().getOrThrow();
final workspaces =
await _userService!.getWorkspaces().getOrThrow();
@@ -12,7 +12,8 @@ part 'folder_bloc.freezed.dart';
enum FolderSpaceType {
favorite,
private,
public;
public,
unknown;
ViewSectionPB get toViewSectionPB {
switch (this) {
@@ -21,6 +22,7 @@ enum FolderSpaceType {
case FolderSpaceType.public:
return ViewSectionPB.Public;
case FolderSpaceType.favorite:
case FolderSpaceType.unknown:
throw UnimplementedError();
}
}
@@ -4,12 +4,14 @@ import 'dart:convert';
import 'package:appflowy/core/config/kv.dart';
import 'package:appflowy/core/config/kv_keys.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/shared/list_extension.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/application/user_service.dart';
import 'package:appflowy/workspace/application/view/prelude.dart';
import 'package:appflowy/workspace/application/view/view_ext.dart';
import 'package:appflowy/workspace/application/view/view_service.dart';
import 'package:appflowy/workspace/application/workspace/prelude.dart';
import 'package:appflowy/workspace/application/workspace/workspace_sections_listener.dart';
import 'package:appflowy/workspace/application/workspace/workspace_service.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/space_icon_popup.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
@@ -20,6 +22,7 @@ import 'package:appflowy_result/appflowy_result.dart';
import 'package:collection/collection.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/uuid.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:protobuf/protobuf.dart';
@@ -68,6 +71,7 @@ class SpaceBloc extends Bloc<SpaceEvent, SpaceState> {
_initial(userProfile, workspaceId);
final (spaces, publicViews, privateViews) = await _getSpaces();
final shouldShowUpgradeDialog = await this.shouldShowUpgradeDialog(
spaces: spaces,
publicViews: publicViews,
@@ -82,13 +86,12 @@ class SpaceBloc extends Bloc<SpaceEvent, SpaceState> {
currentSpace: currentSpace,
isExpanded: isExpanded,
shouldShowUpgradeDialog: shouldShowUpgradeDialog,
isInitialized: true,
),
);
if (shouldShowUpgradeDialog) {
if (!integrationMode().isTest) {
add(const SpaceEvent.migrate());
}
if (shouldShowUpgradeDialog && !integrationMode().isTest) {
add(const SpaceEvent.migrate());
}
if (openFirstPage) {
@@ -124,6 +127,7 @@ class SpaceBloc extends Bloc<SpaceEvent, SpaceState> {
SpaceEvent.createPage(
name: LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
index: 0,
layout: ViewLayoutPB.Document,
),
);
}
@@ -191,13 +195,31 @@ class SpaceBloc extends Bloc<SpaceEvent, SpaceState> {
open: (space) async {
await _openSpace(space);
final isExpanded = await _getSpaceExpandStatus(space);
emit(state.copyWith(currentSpace: space, isExpanded: isExpanded));
final views = await ViewBackendService.getChildViews(
viewId: space.id,
);
final currentSpace = views.fold(
(views) {
space.freeze();
return space.rebuild((b) {
b.childViews.clear();
b.childViews.addAll(views);
});
},
(_) => space,
);
emit(
state.copyWith(
currentSpace: currentSpace,
isExpanded: isExpanded,
),
);
// don't open the page automatically on mobile
if (PlatformExtension.isDesktop) {
// open the first page by default
if (space.childViews.isNotEmpty) {
final firstPage = space.childViews.first;
if (currentSpace.childViews.isNotEmpty) {
final firstPage = currentSpace.childViews.first;
emit(
state.copyWith(
lastCreatedPage: firstPage,
@@ -216,7 +238,7 @@ class SpaceBloc extends Bloc<SpaceEvent, SpaceState> {
await _setSpaceExpandStatus(space, isExpanded);
emit(state.copyWith(isExpanded: isExpanded));
},
createPage: (name, index) async {
createPage: (name, layout, index) async {
final parentViewId = state.currentSpace?.id;
if (parentViewId == null) {
return;
@@ -224,7 +246,7 @@ class SpaceBloc extends Bloc<SpaceEvent, SpaceState> {
final result = await ViewBackendService.createView(
name: name,
layoutType: ViewLayoutPB.Document,
layoutType: layout,
parentViewId: parentViewId,
index: index,
);
@@ -291,6 +313,22 @@ class SpaceBloc extends Bloc<SpaceEvent, SpaceState> {
final nextSpace = spaces[nextIndex];
add(SpaceEvent.open(nextSpace));
},
duplicate: () async {
final currentSpace = state.currentSpace;
if (currentSpace == null) {
return;
}
emit(state.copyWith(isDuplicatingSpace: true));
final newSpace = await _duplicateSpace(currentSpace);
// open the duplicated space
if (newSpace != null) {
add(const SpaceEvent.didReceiveSpaceUpdate());
add(SpaceEvent.open(newSpace));
}
emit(state.copyWith(isDuplicatingSpace: false));
},
);
},
);
@@ -298,6 +336,7 @@ class SpaceBloc extends Bloc<SpaceEvent, SpaceState> {
late WorkspaceService _workspaceService;
String? _workspaceId;
late UserProfilePB userProfile;
WorkspaceSectionsListener? _listener;
@override
@@ -312,8 +351,9 @@ class SpaceBloc extends Bloc<SpaceEvent, SpaceState> {
if (sectionViews == null || sectionViews.views.isEmpty) {
return (<ViewPB>[], <ViewPB>[], <ViewPB>[]);
}
final publicViews = sectionViews.publicViews;
final privateViews = sectionViews.privateViews;
final publicViews = sectionViews.publicViews.unique((e) => e.id);
final privateViews = sectionViews.privateViews.unique((e) => e.id);
final publicSpaces = publicViews.where((e) => e.isSpace);
final privateSpaces = privateViews.where((e) => e.isSpace);
@@ -333,25 +373,22 @@ class SpaceBloc extends Bloc<SpaceEvent, SpaceState> {
SpacePermission.private => ViewSectionPB.Private,
};
final extra = {
ViewExtKeys.isSpaceKey: true,
ViewExtKeys.spaceIconKey: icon,
ViewExtKeys.spaceIconColorKey: iconColor,
ViewExtKeys.spacePermissionKey: permission.index,
ViewExtKeys.spaceCreatedAtKey: DateTime.now().millisecondsSinceEpoch,
};
final result = await _workspaceService.createView(
name: name,
viewSection: section,
setAsCurrent: false,
setAsCurrent: true,
viewId: viewId,
extra: jsonEncode(extra),
);
return await result.fold((space) async {
Log.info('Space created: $space');
final extra = {
ViewExtKeys.isSpaceKey: true,
ViewExtKeys.spaceIconKey: icon,
ViewExtKeys.spaceIconColorKey: iconColor,
ViewExtKeys.spacePermissionKey: permission.index,
ViewExtKeys.spaceCreatedAtKey: DateTime.now().millisecondsSinceEpoch,
};
await ViewBackendService.updateView(
viewId: space.id,
extra: jsonEncode(extra),
);
return space;
}, (error) {
Log.error('Failed to create space: $error');
@@ -389,6 +426,7 @@ class SpaceBloc extends Bloc<SpaceEvent, SpaceState> {
void _initial(UserProfilePB userProfile, String workspaceId) {
_workspaceService = WorkspaceService(workspaceId: workspaceId);
_workspaceId = workspaceId;
this.userProfile = userProfile;
_listener = WorkspaceSectionsListener(
user: userProfile,
@@ -449,7 +487,7 @@ class SpaceBloc extends Bloc<SpaceEvent, SpaceState> {
Future<bool> _getSpaceExpandStatus(ViewPB? space) async {
if (space == null) {
return false;
return true;
}
return getIt<KeyValueStorage>().get(KVKeys.expandedViews).then((result) {
@@ -587,6 +625,39 @@ class SpaceBloc extends Bloc<SpaceEvent, SpaceState> {
return false;
}
Future<ViewPB?> _duplicateSpace(ViewPB space) async {
// if the space is not duplicated, try to create a new space
final icon = space.icon.value.isNotEmpty
? space.icon.value
: builtInSpaceIcons.first;
final iconColor = space.spaceIconColor ?? builtInSpaceColors.first;
final newSpace = await _createSpace(
name: '${space.name} (copy)',
icon: icon,
iconColor: iconColor,
permission: space.spacePermission,
);
if (newSpace == null) {
return null;
}
for (final view in space.childViews) {
await ViewBackendService.duplicate(
view: view,
openAfterDuplicate: true,
syncAfterDuplicate: true,
includeChildren: true,
parentViewId: newSpace.id,
suffix: '',
);
}
Log.info('Space duplicated: $newSpace');
return newSpace;
}
}
@freezed
@@ -606,6 +677,7 @@ class SpaceEvent with _$SpaceEvent {
const factory SpaceEvent.rename(ViewPB space, String name) = _Rename;
const factory SpaceEvent.changeIcon(String icon, String iconColor) =
_ChangeIcon;
const factory SpaceEvent.duplicate() = _Duplicate;
const factory SpaceEvent.update({
String? name,
String? icon,
@@ -616,6 +688,7 @@ class SpaceEvent with _$SpaceEvent {
const factory SpaceEvent.expand(ViewPB space, bool isExpanded) = _Expand;
const factory SpaceEvent.createPage({
required String name,
required ViewLayoutPB layout,
int? index,
}) = _CreatePage;
const factory SpaceEvent.delete(ViewPB? space) = _Delete;
@@ -638,6 +711,8 @@ class SpaceState with _$SpaceState {
@Default(null) ViewPB? lastCreatedPage,
FlowyResult<void, FlowyError>? createPageResult,
@Default(false) bool shouldShowUpgradeDialog,
@Default(false) bool isDuplicatingSpace,
@Default(false) bool isInitialized,
}) = _SpaceState;
factory SpaceState.initial() => const SpaceState();
@@ -0,0 +1,59 @@
import 'package:appflowy/workspace/application/view/view_service.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_result/appflowy_result.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'space_search_bloc.freezed.dart';
class SpaceSearchBloc extends Bloc<SpaceSearchEvent, SpaceSearchState> {
SpaceSearchBloc() : super(SpaceSearchState.initial()) {
on<SpaceSearchEvent>(
(event, emit) async {
await event.when(
initial: () async {
_allViews = await ViewBackendService.getAllViews().fold(
(s) => s.items,
(_) => <ViewPB>[],
);
},
search: (query) {
if (query.isEmpty) {
emit(
state.copyWith(
queryResults: null,
),
);
} else {
final queryResults = _allViews.where(
(view) => view.name.toLowerCase().contains(query.toLowerCase()),
);
emit(
state.copyWith(
queryResults: queryResults.toList(),
),
);
}
},
);
},
);
}
late final List<ViewPB> _allViews;
}
@freezed
class SpaceSearchEvent with _$SpaceSearchEvent {
const factory SpaceSearchEvent.initial() = _Initial;
const factory SpaceSearchEvent.search(String query) = _Search;
}
@freezed
class SpaceSearchState with _$SpaceSearchState {
const factory SpaceSearchState({
List<ViewPB>? queryResults,
}) = _SpaceSearchState;
factory SpaceSearchState.initial() => const SpaceSearchState();
}
@@ -63,24 +63,6 @@ class SettingsUserViewBloc extends Bloc<SettingsUserEvent, SettingsUserState> {
);
});
},
updateUserOpenAIKey: (openAIKey) {
_userService.updateUserProfile(openAIKey: openAIKey).then((result) {
result.fold(
(l) => null,
(err) => Log.error(err),
);
});
},
updateUserStabilityAIKey: (stabilityAIKey) {
_userService
.updateUserProfile(stabilityAiKey: stabilityAIKey)
.then((result) {
result.fold(
(l) => null,
(err) => Log.error(err),
);
});
},
updateUserEmail: (String email) {
_userService.updateUserProfile(email: email).then((result) {
result.fold(
@@ -127,11 +109,6 @@ class SettingsUserEvent with _$SettingsUserEvent {
const factory SettingsUserEvent.updateUserIcon({required String iconUrl}) =
_UpdateUserIcon;
const factory SettingsUserEvent.removeUserIcon() = _RemoveUserIcon;
const factory SettingsUserEvent.updateUserOpenAIKey(String openAIKey) =
_UpdateUserOpenaiKey;
const factory SettingsUserEvent.updateUserStabilityAIKey(
String stabilityAIKey,
) = _UpdateUserStabilityAIKey;
const factory SettingsUserEvent.didReceiveUserProfile(
UserProfilePB newUserProfile,
) = _DidReceiveUserProfile;
@@ -29,9 +29,9 @@ class UserWorkspaceBloc extends Bloc<UserWorkspaceEvent, UserWorkspaceState> {
await event.when(
initial: () async {
_listener.start(
didUpdateUserWorkspaces: (workspaces) =>
onUserWorkspaceListUpdated: (workspaces) =>
add(UserWorkspaceEvent.updateWorkspaces(workspaces)),
didUpdateUserWorkspace: (workspace) {
onUserWorkspaceUpdated: (workspace) {
// If currentWorkspace is updated, eg. Icon or Name, we should notify
// the UI to render the updated information.
final currentWorkspace = state.currentWorkspace;
@@ -56,23 +56,21 @@ class UserWorkspaceBloc extends Bloc<UserWorkspaceEvent, UserWorkspaceState> {
await _userService.openWorkspace(currentWorkspace.workspaceId);
}
WorkspaceMemberPB? currentWorkspaceMember;
final workspaceMemberResult =
await _userService.getWorkspaceMember();
currentWorkspaceMember = workspaceMemberResult.fold(
(s) => s,
(e) => null,
);
emit(
state.copyWith(
currentWorkspace: currentWorkspace,
workspaces: workspaces,
isCollabWorkspaceOn: isCollabWorkspaceOn,
currentWorkspaceMember: currentWorkspaceMember,
actionResult: null,
),
);
/// We wait with fetching the workspace member as it may take some time,
/// to avoid blocking the UI from rendering (the sidebar).
final workspaceMemberResult =
await _userService.getWorkspaceMember();
final workspaceMember = workspaceMemberResult.toNullable();
emit(state.copyWith(currentWorkspaceMember: workspaceMember));
},
fetchWorkspaces: () async {
final result = await _fetchWorkspaces();
@@ -209,14 +207,6 @@ class UserWorkspaceBloc extends Bloc<UserWorkspaceEvent, UserWorkspaceState> {
(e) => state.currentWorkspace,
);
WorkspaceMemberPB? currentWorkspaceMember;
final workspaceMemberResult =
await _userService.getWorkspaceMember();
currentWorkspaceMember = workspaceMemberResult.fold(
(s) => s,
(e) => null,
);
result
..onSuccess((s) {
Log.info(
@@ -230,7 +220,6 @@ class UserWorkspaceBloc extends Bloc<UserWorkspaceEvent, UserWorkspaceState> {
emit(
state.copyWith(
currentWorkspace: currentWorkspace,
currentWorkspaceMember: currentWorkspaceMember,
actionResult: UserWorkspaceActionResult(
actionType: UserWorkspaceActionType.open,
isLoading: false,
@@ -238,6 +227,14 @@ class UserWorkspaceBloc extends Bloc<UserWorkspaceEvent, UserWorkspaceState> {
),
),
);
/// We wait with fetching the workspace member as it may take some time,
/// to avoid blocking the UI from rendering (the sidebar).
final workspaceMemberResult =
await _userService.getWorkspaceMember();
final workspaceMember = workspaceMemberResult.toNullable();
emit(state.copyWith(currentWorkspaceMember: workspaceMember));
},
renameWorkspace: (workspaceId, name) async {
final result =
@@ -406,7 +403,7 @@ class UserWorkspaceBloc extends Bloc<UserWorkspaceEvent, UserWorkspaceState> {
)> _fetchWorkspaces() async {
try {
final currentWorkspace =
await _userService.getCurrentWorkspace().getOrThrow();
await UserBackendService.getCurrentWorkspace().getOrThrow();
final workspaces = await _userService.getWorkspaces().getOrThrow();
if (workspaces.isEmpty) {
workspaces.add(convertWorkspacePBToUserWorkspace(currentWorkspace));
@@ -512,6 +509,7 @@ class UserWorkspaceState with _$UserWorkspaceState {
if (identical(this, other)) return true;
return other is UserWorkspaceState &&
other.currentWorkspaceMember == currentWorkspaceMember &&
other.currentWorkspace == currentWorkspace &&
_deepCollectionEquality.equals(other.workspaces, workspaces) &&
identical(other.actionResult, actionResult);
@@ -1,3 +1,4 @@
import 'dart:async';
import 'dart:convert';
import 'package:appflowy/core/config/kv.dart';
@@ -7,6 +8,7 @@ import 'package:appflowy/workspace/application/favorite/favorite_listener.dart';
import 'package:appflowy/workspace/application/recent/cached_recent_service.dart';
import 'package:appflowy/workspace/application/view/view_listener.dart';
import 'package:appflowy/workspace/application/view/view_service.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
@@ -138,11 +140,18 @@ class ViewBloc extends Bloc<ViewEvent, ViewState> {
);
},
delete: (e) async {
// unpublish the page and all its child pages if they are published
await _unpublishPage(view);
final result = await ViewBackendService.delete(viewId: view.id);
emit(
result.fold(
(l) =>
state.copyWith(successOrFailure: FlowyResult.success(null)),
(l) {
return state.copyWith(
successOrFailure: FlowyResult.success(null),
);
},
(error) => state.copyWith(
successOrFailure: FlowyResult.failure(error),
),
@@ -154,7 +163,12 @@ class ViewBloc extends Bloc<ViewEvent, ViewState> {
);
},
duplicate: (e) async {
final result = await ViewBackendService.duplicate(view: view);
final result = await ViewBackendService.duplicate(
view: view,
openAfterDuplicate: true,
syncAfterDuplicate: true,
includeChildren: true,
);
emit(
result.fold(
(l) =>
@@ -175,8 +189,11 @@ class ViewBloc extends Bloc<ViewEvent, ViewState> {
);
emit(
result.fold(
(l) =>
state.copyWith(successOrFailure: FlowyResult.success(null)),
(l) {
return state.copyWith(
successOrFailure: FlowyResult.success(null),
);
},
(error) => state.copyWith(
successOrFailure: FlowyResult.failure(error),
),
@@ -231,6 +248,13 @@ class ViewBloc extends Bloc<ViewEvent, ViewState> {
}
add(const ViewEvent.setIsExpanded(false));
},
unpublish: (value) async {
if (value.sync) {
await _unpublishPage(view);
} else {
unawaited(_unpublishPage(view));
}
},
);
},
);
@@ -378,6 +402,20 @@ class ViewBloc extends Bloc<ViewEvent, ViewState> {
return null;
}
// unpublish the page and all its child pages
Future<void> _unpublishPage(ViewPB views) async {
final (_, publishedPages) = await ViewBackendService.containPublishedPage(
view,
);
await Future.wait(
publishedPages.map((view) async {
Log.info('unpublishing page: ${view.id}, ${view.name}');
await ViewBackendService.unpublish(view);
}),
);
}
bool _isSameViewIgnoreChildren(ViewPB from, ViewPB to) {
return _hash(from) == _hash(to);
}
@@ -425,6 +463,8 @@ class ViewEvent with _$ViewEvent {
) = UpdateViewVisibility;
const factory ViewEvent.updateIcon(String? icon) = UpdateIcon;
const factory ViewEvent.collapseAllPages() = CollapseAllPages;
// this event will unpublish the page and all its child pages if they are published
const factory ViewEvent.unpublish({required bool sync}) = Unpublish;
}
@freezed
@@ -133,13 +133,13 @@ extension ViewExtension on ViewPB {
}
}
FlowySvg get spaceIconSvg {
FlowySvg? get spaceIconSvg {
try {
final ext = jsonDecode(extra);
final icon = ext[ViewExtKeys.spaceIconKey];
final color = ext[ViewExtKeys.spaceIconColorKey];
if (icon == null || color == null) {
return const FlowySvg(FlowySvgs.space_icon_s, blendMode: null);
return null;
}
return FlowySvg(
FlowySvgData('assets/flowy_icons/16x/$icon.svg'),
@@ -147,7 +147,7 @@ extension ViewExtension on ViewPB {
blendMode: BlendMode.srcOut,
);
} catch (e) {
return const FlowySvg(FlowySvgs.space_icon_s, blendMode: null);
return null;
}
}
Loaded 100 of 527 files, more files were not shown because too many files have changed in this diff. Show more