Compare commits
31
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
565485265b | ||
|
|
f6f615a54c | ||
|
|
17d88f2ad3 | ||
|
|
8f66246e25 | ||
|
|
063ec186bb | ||
|
|
ec444d4a00 | ||
|
|
95475cb870 | ||
|
|
0520855925 | ||
|
|
61704f6d4a | ||
|
|
b85990e453 | ||
|
|
b0d525a48f | ||
|
|
bf048ffc0d | ||
|
|
1f44d4de33 | ||
|
|
653f88033e | ||
|
|
0c88c7ce2f | ||
|
|
f38688b17e | ||
|
|
1a3701feaf | ||
|
|
1b86587875 | ||
|
|
120db6805f | ||
|
|
6b14983f2b | ||
|
|
9a4cde07b6 | ||
|
|
d6d591f359 | ||
|
|
0570615414 | ||
|
|
609bf2a4ce | ||
|
|
75231e606c | ||
|
|
dd9c3dd2a3 | ||
|
|
4821d7ea65 | ||
|
|
874d1e86c2 | ||
|
|
b7ec7ce32e | ||
|
|
0141538be8 | ||
|
|
dcf48ceb50 |
No files matched your search
@@ -324,6 +324,7 @@ jobs:
|
||||
with:
|
||||
upload_url: ${{ needs.create-release.outputs.upload_url }}
|
||||
asset_path: ${{ env.LINUX_APP_RELEASE_PATH }}/${{ env.LINUX_PACKAGE_NAME }}
|
||||
|
||||
asset_name: ${{ env.LINUX_PACKAGE_NAME }}
|
||||
asset_content_type: application/octet-stream
|
||||
|
||||
@@ -352,3 +353,12 @@ jobs:
|
||||
tags: ${{ secrets.DOCKER_HUB_USERNAME }}/appflowy_client:${{ github.ref_name }}
|
||||
cache-from: type=registry,ref=${{ secrets.DOCKER_HUB_USERNAME }}/af_build_cache:buildcache
|
||||
cache-to: type=registry,ref=${{ secrets.DOCKER_HUB_USERNAME }}/af_build_cache:buildcache,mode=max
|
||||
|
||||
notify-discord:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [build-for-linux, build-for-windows, build-for-macOS]
|
||||
steps:
|
||||
- name: Notify Discord
|
||||
run: |
|
||||
curl -H "Content-Type: application/json" -d '{"username": "release@appflowy", "content": "🎉 AppFlowy ${{ github.ref_name }} is available. https://github.com/LucasXu0/AppFlowy/releases/tag/'${{ github.ref_name }}'"}' "https://discord.com/api/webhooks/${{ secrets.DISCORD }}"
|
||||
shell: bash
|
||||
@@ -1,5 +1,24 @@
|
||||
# Release Notes
|
||||
|
||||
## Version 0.1.2 - 03/28/2023
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fix: update calendar selected range.
|
||||
- Fix: duplicate view.
|
||||
|
||||
## Version 0.1.1 - 03/21/2023
|
||||
|
||||
### New features
|
||||
|
||||
- AppFlowy brings the power of OpenAI into your AppFlowy pages. Ask AI to write anything for you in AppFlowy.
|
||||
- Support adding a cover image to your page, making your pages beautiful.
|
||||
- More shortcuts become available. Click on '?' at the bottom right to access our shortcut guide.
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fix some bugs
|
||||
|
||||
## Version 0.1.0 - 02/09/2023
|
||||
|
||||
### New features
|
||||
|
||||
Vendored
+2
-2
@@ -12,8 +12,8 @@
|
||||
"type": "dart",
|
||||
"preLaunchTask": "AF: Build Appflowy Core",
|
||||
"env": {
|
||||
// "RUST_LOG": "trace",
|
||||
"RUST_LOG": "debug"
|
||||
"RUST_LOG": "trace",
|
||||
// "RUST_LOG": "debug"
|
||||
},
|
||||
"cwd": "${workspaceRoot}/appflowy_flutter"
|
||||
},
|
||||
|
||||
@@ -23,7 +23,7 @@ CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true
|
||||
CARGO_MAKE_CRATE_FS_NAME = "dart_ffi"
|
||||
CARGO_MAKE_CRATE_NAME = "dart-ffi"
|
||||
LIB_NAME = "dart_ffi"
|
||||
CURRENT_APP_VERSION = "0.1.0"
|
||||
CURRENT_APP_VERSION = "0.1.2"
|
||||
FLUTTER_DESKTOP_FEATURES = "dart,rev-sqlite"
|
||||
PRODUCT_NAME = "AppFlowy"
|
||||
# CRATE_TYPE: https://doc.rust-lang.org/reference/linkage.html
|
||||
|
||||
@@ -1,10 +1,30 @@
|
||||
# This file tracks properties of this Flutter project.
|
||||
# Used by Flutter tool to assess capabilities and perform upgrades etc.
|
||||
#
|
||||
# This file should be version controlled and should not be manually edited.
|
||||
# This file should be version controlled.
|
||||
|
||||
version:
|
||||
revision: fa5883b78e566877613ad1ccb48dd92075cb5c23
|
||||
channel: dev
|
||||
revision: 135454af32477f815a7525073027a3ff9eff1bfd
|
||||
channel: stable
|
||||
|
||||
project_type: app
|
||||
|
||||
# Tracks metadata for the flutter migrate command
|
||||
migration:
|
||||
platforms:
|
||||
- platform: root
|
||||
create_revision: 135454af32477f815a7525073027a3ff9eff1bfd
|
||||
base_revision: 135454af32477f815a7525073027a3ff9eff1bfd
|
||||
- platform: windows
|
||||
create_revision: 135454af32477f815a7525073027a3ff9eff1bfd
|
||||
base_revision: 135454af32477f815a7525073027a3ff9eff1bfd
|
||||
|
||||
# User provided section
|
||||
|
||||
# List of Local paths (relative to this file) that should be
|
||||
# ignored by the migrate tool.
|
||||
#
|
||||
# Files that are not part of the templates will be ignored by default.
|
||||
unmanaged_files:
|
||||
- 'lib/main.dart'
|
||||
- 'ios/Runner.xcodeproj/project.pbxproj'
|
||||
@@ -45,7 +45,7 @@ android {
|
||||
|
||||
defaultConfig {
|
||||
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
||||
applicationId "com.example.appflowy_flutter"
|
||||
applicationId "io.appflowy.appflowy"
|
||||
minSdkVersion 19
|
||||
targetSdkVersion 31
|
||||
versionCode flutterVersionCode.toInteger()
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.example.appflowy_flutter">
|
||||
package="io.appflowy.appflowy">
|
||||
<!-- Flutter needs it to communicate with the running application
|
||||
to allow setting breakpoints, to provide hot reload, etc.
|
||||
-->
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.example.appflowy_flutter">
|
||||
package="io.appflowy.appflowy">
|
||||
<application
|
||||
android:label="appflowy_flutter"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package com.example.appflowy_flutter
|
||||
package io.appflowy.appflowy
|
||||
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.example.appflowy_flutter">
|
||||
package="io.appflowy.appflowy">
|
||||
<!-- Flutter needs it to communicate with the running application
|
||||
to allow setting breakpoints, to provide hot reload, etc.
|
||||
-->
|
||||
|
||||
@@ -138,7 +138,8 @@
|
||||
"keep": "Keep",
|
||||
"tryAgain": "Try again",
|
||||
"discard": "Discard",
|
||||
"replace": "Replace"
|
||||
"replace": "Replace",
|
||||
"insertBelow": "Insert Below"
|
||||
},
|
||||
"label": {
|
||||
"welcome": "Welcome!",
|
||||
@@ -342,18 +343,20 @@
|
||||
"plugins": {
|
||||
"referencedBoard": "Referenced Board",
|
||||
"referencedGrid": "Referenced Grid",
|
||||
"autoCompletionMenuItemName": "Auto Completion",
|
||||
"autoGeneratorMenuItemName": "Auto Generator",
|
||||
"autoGeneratorTitleName": "Open AI: Auto Generator",
|
||||
"autoGeneratorMenuItemName": "OpenAI Writer",
|
||||
"autoGeneratorTitleName": "OpenAI: Ask AI to write anything...",
|
||||
"autoGeneratorLearnMore": "Learn more",
|
||||
"autoGeneratorGenerate": "Generate",
|
||||
"autoGeneratorHintText": "Tell us what you want to generate by OpenAI ...",
|
||||
"autoGeneratorHintText": "Ask OpenAI ...",
|
||||
"autoGeneratorCantGetOpenAIKey": "Can't get OpenAI key",
|
||||
"smartEditTitleName": "Open AI: Smart Edit",
|
||||
"smartEdit": "AI Assistants",
|
||||
"openAI": "OpenAI",
|
||||
"smartEditFixSpelling": "Fix spelling",
|
||||
"warning": "⚠️ AI responses can be inaccurate or misleading.",
|
||||
"smartEditSummarize": "Summarize",
|
||||
"smartEditCouldNotFetchResult": "Could not fetch result from OpenAI",
|
||||
"smartEditCouldNotFetchKey": "Could not fetch OpenAI key",
|
||||
"smartEditDisabled": "Connect OpenAI in Settings",
|
||||
"cover": {
|
||||
"changeCover": "Change Cover",
|
||||
"colors": "Colors",
|
||||
@@ -361,7 +364,8 @@
|
||||
"abstract": "Abstract",
|
||||
"addCover": "Add Cover",
|
||||
"addLocalImage": "Add local image"
|
||||
}
|
||||
},
|
||||
"discardResponse": "Do you want to discard the AI responses?"
|
||||
}
|
||||
},
|
||||
"board": {
|
||||
|
||||
@@ -349,7 +349,6 @@
|
||||
"autoGeneratorGenerate": "Gerar",
|
||||
"autoGeneratorHintText": "Diga-nos o que você deseja gerar por IA ...",
|
||||
"autoGeneratorCantGetOpenAIKey": "Não foi possível obter a chave da OpenAI",
|
||||
"smartEditTitleName": "IA: edição inteligente",
|
||||
"smartEditFixSpelling": "Corrigir ortografia",
|
||||
"smartEditSummarize": "Resumir",
|
||||
"smartEditCouldNotFetchResult": "Não foi possível obter o resultado do OpenAI",
|
||||
|
||||
@@ -359,7 +359,7 @@
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.appFlowy;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = io.appflowy.appflowy;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
SWIFT_VERSION = 5.0;
|
||||
@@ -483,7 +483,7 @@
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.appFlowy;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = io.appflowy.appflowy;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
@@ -502,7 +502,7 @@
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.appFlowy;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = io.appflowy.appflowy;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
SWIFT_VERSION = 5.0;
|
||||
|
||||
+2
-3
@@ -23,9 +23,8 @@ import '../../../../grid/presentation/widgets/common/type_option_separator.dart'
|
||||
import '../../../../grid/presentation/widgets/header/type_option/date.dart';
|
||||
import 'date_cal_bloc.dart';
|
||||
|
||||
final kToday = DateTime.now();
|
||||
final kFirstDay = DateTime(kToday.year, kToday.month - 3, kToday.day);
|
||||
final kLastDay = DateTime(kToday.year, kToday.month + 3, kToday.day);
|
||||
final kFirstDay = DateTime.utc(1970, 1, 1);
|
||||
final kLastDay = DateTime.utc(2100, 1, 1);
|
||||
|
||||
class DateCellEditor extends StatefulWidget {
|
||||
final VoidCallback onDismissed;
|
||||
|
||||
@@ -183,9 +183,7 @@ class _AppFlowyEditorPageState extends State<_AppFlowyEditorPage> {
|
||||
],
|
||||
],
|
||||
toolbarItems: [
|
||||
if (openAIKey != null && openAIKey!.isNotEmpty) ...[
|
||||
smartEditItem,
|
||||
]
|
||||
smartEditItem,
|
||||
],
|
||||
themeData: theme.copyWith(extensions: [
|
||||
...theme.extensions.values,
|
||||
|
||||
+3
-2
@@ -16,6 +16,7 @@ import 'package:flowy_infra_ui/style_widget/icon_button.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
|
||||
const String kLocalImagesKey = 'local_images';
|
||||
|
||||
@@ -263,7 +264,7 @@ class _ChangeCoverPopoverState extends State<ChangeCoverPopover> {
|
||||
if (path != null) {
|
||||
final directory = await _coverPath();
|
||||
final newPath = await File(path).copy(
|
||||
'$directory/${path.split('/').last}',
|
||||
'$directory/${path.split(path).last}}',
|
||||
);
|
||||
imageNames.add(newPath.path);
|
||||
}
|
||||
@@ -274,7 +275,7 @@ class _ChangeCoverPopoverState extends State<ChangeCoverPopover> {
|
||||
|
||||
Future<String> _coverPath() async {
|
||||
final directory = await getIt<SettingsLocationCubit>().fetchLocation();
|
||||
return Directory('$directory/covers')
|
||||
return Directory(path.join(directory, 'covers'))
|
||||
.create(recursive: true)
|
||||
.then((value) => value.path);
|
||||
}
|
||||
|
||||
+17
-8
@@ -1,7 +1,6 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:appflowy/plugins/document/presentation/plugins/openai/service/text_edit.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
|
||||
import 'text_completion.dart';
|
||||
import 'package:dartz/dartz.dart';
|
||||
@@ -125,6 +124,7 @@ class HttpOpenAIRepository implements OpenAIRepository {
|
||||
String? suffix,
|
||||
int maxTokens = 2048,
|
||||
double temperature = 0.3,
|
||||
bool useAction = false,
|
||||
}) async {
|
||||
final parameters = {
|
||||
'model': 'text-davinci-003',
|
||||
@@ -151,14 +151,22 @@ class HttpOpenAIRepository implements OpenAIRepository {
|
||||
.transform(const Utf8Decoder())
|
||||
.transform(const LineSplitter())) {
|
||||
syntax += 1;
|
||||
if (syntax == 3) {
|
||||
await onStart();
|
||||
continue;
|
||||
} else if (syntax < 3) {
|
||||
continue;
|
||||
if (!useAction) {
|
||||
if (syntax == 3) {
|
||||
await onStart();
|
||||
continue;
|
||||
} else if (syntax < 3) {
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
if (syntax == 2) {
|
||||
await onStart();
|
||||
continue;
|
||||
} else if (syntax < 2) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
final data = chunk.trim().split('data: ');
|
||||
Log.editor.info(data.toString());
|
||||
if (data.length > 1) {
|
||||
if (data[1] != '[DONE]') {
|
||||
final response = TextCompletionResponse.fromJson(
|
||||
@@ -173,7 +181,7 @@ class HttpOpenAIRepository implements OpenAIRepository {
|
||||
previousSyntax = response.choices.first.text;
|
||||
}
|
||||
} else {
|
||||
onEnd();
|
||||
await onEnd();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -183,6 +191,7 @@ class HttpOpenAIRepository implements OpenAIRepository {
|
||||
OpenAIError.fromJson(json.decode(body)['error']),
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
Future<void> openLearnMorePage() async {
|
||||
final uri = Uri.parse(
|
||||
'https://appflowy.gitbook.io/docs/essential-documentation/appflowy-x-openai');
|
||||
if (await canLaunchUrl(uri)) {
|
||||
await launchUrl(uri);
|
||||
}
|
||||
}
|
||||
+60
-38
@@ -1,6 +1,8 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:appflowy/plugins/document/presentation/plugins/openai/service/openai_client.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/plugins/openai/util/learn_more_action.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/plugins/openai/widgets/discard_dialog.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/plugins/openai/widgets/loading.dart';
|
||||
import 'package:appflowy/user/application/user_service.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
@@ -9,7 +11,7 @@ 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/services.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
@@ -56,6 +58,7 @@ class _AutoCompletionInputState extends State<_AutoCompletionInput> {
|
||||
final controller = TextEditingController();
|
||||
final focusNode = FocusNode();
|
||||
final textFieldFocusNode = FocusNode();
|
||||
final interceptor = SelectionInterceptor();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -63,6 +66,34 @@ class _AutoCompletionInputState extends State<_AutoCompletionInput> {
|
||||
|
||||
textFieldFocusNode.addListener(_onFocusChanged);
|
||||
textFieldFocusNode.requestFocus();
|
||||
widget.editorState.service.selectionService.register(interceptor
|
||||
..canTap = (details) {
|
||||
final renderBox = context.findRenderObject() as RenderBox?;
|
||||
if (renderBox != null) {
|
||||
if (!isTapDownDetailsInRenderBox(details, renderBox)) {
|
||||
if (text.isNotEmpty || controller.text.isNotEmpty) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return DiscardDialog(
|
||||
onConfirm: () => _onDiscard(),
|
||||
onCancel: () {},
|
||||
);
|
||||
},
|
||||
);
|
||||
} else if (controller.text.isEmpty) {
|
||||
_onExit();
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
bool isTapDownDetailsInRenderBox(TapDownDetails details, RenderBox box) {
|
||||
var result = BoxHitTestResult();
|
||||
box.hitTest(result, position: box.globalToLocal(details.globalPosition));
|
||||
return result.path.any((entry) => entry.target == box);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -71,6 +102,7 @@ class _AutoCompletionInputState extends State<_AutoCompletionInput> {
|
||||
textFieldFocusNode.removeListener(_onFocusChanged);
|
||||
widget.editorState.service.selectionService.currentSelection
|
||||
.removeListener(_onCancelWhenSelectionChanged);
|
||||
widget.editorState.service.selectionService.unRegister(interceptor);
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
@@ -119,34 +151,26 @@ class _AutoCompletionInputState extends State<_AutoCompletionInput> {
|
||||
fontSize: 14,
|
||||
),
|
||||
const Spacer(),
|
||||
FlowyText.regular(
|
||||
LocaleKeys.document_plugins_autoGeneratorLearnMore.tr(),
|
||||
),
|
||||
FlowyButton(
|
||||
useIntrinsicWidth: true,
|
||||
text: FlowyText.regular(
|
||||
LocaleKeys.document_plugins_autoGeneratorLearnMore.tr(),
|
||||
),
|
||||
onTap: () async {
|
||||
await openLearnMorePage();
|
||||
},
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildInputWidget(BuildContext context) {
|
||||
return RawKeyboardListener(
|
||||
focusNode: focusNode,
|
||||
onKey: (RawKeyEvent event) async {
|
||||
if (event is! RawKeyDownEvent) return;
|
||||
if (event.logicalKey == LogicalKeyboardKey.enter) {
|
||||
if (controller.text.isNotEmpty) {
|
||||
textFieldFocusNode.unfocus();
|
||||
await _onGenerate();
|
||||
}
|
||||
} else if (event.logicalKey == LogicalKeyboardKey.escape) {
|
||||
await _onExit();
|
||||
}
|
||||
},
|
||||
child: FlowyTextField(
|
||||
hintText: LocaleKeys.document_plugins_autoGeneratorHintText.tr(),
|
||||
controller: controller,
|
||||
maxLines: 3,
|
||||
focusNode: textFieldFocusNode,
|
||||
autoFocus: false,
|
||||
),
|
||||
return FlowyTextField(
|
||||
hintText: LocaleKeys.document_plugins_autoGeneratorHintText.tr(),
|
||||
controller: controller,
|
||||
maxLines: 3,
|
||||
focusNode: textFieldFocusNode,
|
||||
autoFocus: false,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -157,15 +181,9 @@ class _AutoCompletionInputState extends State<_AutoCompletionInput> {
|
||||
TextSpan(
|
||||
children: [
|
||||
TextSpan(
|
||||
text: '${LocaleKeys.button_generate.tr()} ',
|
||||
text: LocaleKeys.button_generate.tr(),
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
TextSpan(
|
||||
text: '↵',
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
onPressed: () async => await _onGenerate(),
|
||||
@@ -175,19 +193,23 @@ class _AutoCompletionInputState extends State<_AutoCompletionInput> {
|
||||
TextSpan(
|
||||
children: [
|
||||
TextSpan(
|
||||
text: '${LocaleKeys.button_Cancel.tr()} ',
|
||||
text: LocaleKeys.button_Cancel.tr(),
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
TextSpan(
|
||||
text: LocaleKeys.button_esc.tr(),
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
onPressed: () async => await _onExit(),
|
||||
),
|
||||
Expanded(
|
||||
child: Container(
|
||||
alignment: Alignment.centerRight,
|
||||
child: FlowyText.regular(
|
||||
LocaleKeys.document_plugins_warning.tr(),
|
||||
color: Theme.of(context).hintColor,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
+5
-2
@@ -2,10 +2,13 @@ import 'package:appflowy/plugins/document/presentation/plugins/openai/widgets/au
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
|
||||
SelectionMenuItem autoGeneratorMenuItem = SelectionMenuItem.node(
|
||||
name: 'Auto Generator',
|
||||
name: LocaleKeys.document_plugins_autoGeneratorMenuItemName.tr(),
|
||||
iconData: Icons.generating_tokens,
|
||||
keywords: ['autogenerator', 'auto generator'],
|
||||
keywords: ['ai', 'openai' 'writer', 'autogenerator'],
|
||||
nodeBuilder: (editorState) {
|
||||
final node = Node(
|
||||
type: kAutoCompletionInputType,
|
||||
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
|
||||
class DiscardDialog extends StatelessWidget {
|
||||
const DiscardDialog({
|
||||
super.key,
|
||||
required this.onConfirm,
|
||||
required this.onCancel,
|
||||
});
|
||||
|
||||
final VoidCallback onConfirm;
|
||||
final VoidCallback onCancel;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return NavigatorOkCancelDialog(
|
||||
message: LocaleKeys.document_plugins_discardResponse.tr(),
|
||||
okTitle: LocaleKeys.button_discard.tr(),
|
||||
cancelTitle: LocaleKeys.button_Cancel.tr(),
|
||||
onOkPressed: onConfirm,
|
||||
onCancelPressed: onCancel,
|
||||
);
|
||||
}
|
||||
}
|
||||
+31
-8
@@ -10,9 +10,37 @@ enum SmartEditAction {
|
||||
String get toInstruction {
|
||||
switch (this) {
|
||||
case SmartEditAction.summarize:
|
||||
return 'Make it shorter';
|
||||
return 'Tl;dr';
|
||||
case SmartEditAction.fixSpelling:
|
||||
return 'Fix all the spelling mistakes';
|
||||
return 'Correct this to standard English:';
|
||||
}
|
||||
}
|
||||
|
||||
String prompt(String input) {
|
||||
switch (this) {
|
||||
case SmartEditAction.summarize:
|
||||
return '$input\n\nTl;dr';
|
||||
case SmartEditAction.fixSpelling:
|
||||
return 'Correct this to standard English:\n\n$input';
|
||||
}
|
||||
}
|
||||
|
||||
static SmartEditAction from(int index) {
|
||||
switch (index) {
|
||||
case 0:
|
||||
return SmartEditAction.summarize;
|
||||
case 1:
|
||||
return SmartEditAction.fixSpelling;
|
||||
}
|
||||
return SmartEditAction.fixSpelling;
|
||||
}
|
||||
|
||||
String get name {
|
||||
switch (this) {
|
||||
case SmartEditAction.summarize:
|
||||
return LocaleKeys.document_plugins_smartEditSummarize.tr();
|
||||
case SmartEditAction.fixSpelling:
|
||||
return LocaleKeys.document_plugins_smartEditFixSpelling.tr();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -26,11 +54,6 @@ class SmartEditActionWrapper extends ActionCell {
|
||||
|
||||
@override
|
||||
String get name {
|
||||
switch (inner) {
|
||||
case SmartEditAction.summarize:
|
||||
return LocaleKeys.document_plugins_smartEditSummarize.tr();
|
||||
case SmartEditAction.fixSpelling:
|
||||
return LocaleKeys.document_plugins_smartEditFixSpelling.tr();
|
||||
}
|
||||
return inner.name;
|
||||
}
|
||||
}
|
||||
+229
-114
@@ -1,19 +1,18 @@
|
||||
import 'package:appflowy/plugins/document/presentation/plugins/openai/service/error.dart';
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:appflowy/plugins/document/presentation/plugins/openai/service/openai_client.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/plugins/openai/service/text_edit.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/plugins/openai/util/learn_more_action.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/plugins/openai/widgets/discard_dialog.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/plugins/openai/widgets/smart_edit_action.dart';
|
||||
import 'package:appflowy/user/application/user_service.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/button.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||
import 'package:flowy_infra_ui/widget/spacing.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.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:flutter/services.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:dartz/dartz.dart' as dartz;
|
||||
import 'package:appflowy/util/either_extension.dart';
|
||||
|
||||
const String kSmartEditType = 'smart_edit_input';
|
||||
const String kSmartEditInstructionType = 'smart_edit_instruction';
|
||||
@@ -22,15 +21,15 @@ const String kSmartEditInputType = 'smart_edit_input';
|
||||
class SmartEditInputBuilder extends NodeWidgetBuilder<Node> {
|
||||
@override
|
||||
NodeValidator<Node> get nodeValidator => (node) {
|
||||
return SmartEditAction.values.map((e) => e.toInstruction).contains(
|
||||
node.attributes[kSmartEditInstructionType],
|
||||
) &&
|
||||
return SmartEditAction.values
|
||||
.map((e) => e.index)
|
||||
.contains(node.attributes[kSmartEditInstructionType]) &&
|
||||
node.attributes[kSmartEditInputType] is String;
|
||||
};
|
||||
|
||||
@override
|
||||
Widget build(NodeWidgetContext<Node> context) {
|
||||
return _SmartEditInput(
|
||||
return _HoverSmartInput(
|
||||
key: context.node.key,
|
||||
node: context.node,
|
||||
editorState: context.editorState,
|
||||
@@ -38,28 +37,111 @@ class SmartEditInputBuilder extends NodeWidgetBuilder<Node> {
|
||||
}
|
||||
}
|
||||
|
||||
class _SmartEditInput extends StatefulWidget {
|
||||
final Node node;
|
||||
|
||||
final EditorState editorState;
|
||||
const _SmartEditInput({
|
||||
Key? key,
|
||||
class _HoverSmartInput extends StatefulWidget {
|
||||
const _HoverSmartInput({
|
||||
required super.key,
|
||||
required this.node,
|
||||
required this.editorState,
|
||||
});
|
||||
|
||||
final Node node;
|
||||
final EditorState editorState;
|
||||
|
||||
@override
|
||||
State<_HoverSmartInput> createState() => _HoverSmartInputState();
|
||||
}
|
||||
|
||||
class _HoverSmartInputState extends State<_HoverSmartInput> {
|
||||
final popoverController = PopoverController();
|
||||
final key = GlobalKey(debugLabel: 'smart_edit_input');
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
|
||||
popoverController.show();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final width = _maxWidth();
|
||||
|
||||
return AppFlowyPopover(
|
||||
controller: popoverController,
|
||||
direction: PopoverDirection.bottomWithLeftAligned,
|
||||
triggerActions: PopoverTriggerFlags.none,
|
||||
margin: EdgeInsets.zero,
|
||||
constraints: BoxConstraints(maxWidth: width),
|
||||
decoration: FlowyDecoration.decoration(
|
||||
Colors.transparent,
|
||||
Colors.transparent,
|
||||
),
|
||||
child: const SizedBox(
|
||||
width: double.infinity,
|
||||
),
|
||||
canClose: () async {
|
||||
final completer = Completer<bool>();
|
||||
final state = key.currentState as _SmartEditInputState;
|
||||
if (state.result.isEmpty) {
|
||||
completer.complete(true);
|
||||
} else {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return DiscardDialog(
|
||||
onConfirm: () => completer.complete(true),
|
||||
onCancel: () => completer.complete(false),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
return completer.future;
|
||||
},
|
||||
popupBuilder: (BuildContext popoverContext) {
|
||||
return _SmartEditInput(
|
||||
key: key,
|
||||
node: widget.node,
|
||||
editorState: widget.editorState,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
double _maxWidth() {
|
||||
var width = double.infinity;
|
||||
final editorSize = widget.editorState.renderBox?.size;
|
||||
final padding = widget.editorState.editorStyle.padding;
|
||||
if (editorSize != null && padding != null) {
|
||||
width = editorSize.width - padding.left - padding.right;
|
||||
}
|
||||
return width;
|
||||
}
|
||||
}
|
||||
|
||||
class _SmartEditInput extends StatefulWidget {
|
||||
const _SmartEditInput({
|
||||
required super.key,
|
||||
required this.node,
|
||||
required this.editorState,
|
||||
});
|
||||
|
||||
final Node node;
|
||||
final EditorState editorState;
|
||||
|
||||
@override
|
||||
State<_SmartEditInput> createState() => _SmartEditInputState();
|
||||
}
|
||||
|
||||
class _SmartEditInputState extends State<_SmartEditInput> {
|
||||
String get instruction => widget.node.attributes[kSmartEditInstructionType];
|
||||
SmartEditAction get action =>
|
||||
SmartEditAction.from(widget.node.attributes[kSmartEditInstructionType]);
|
||||
String get input => widget.node.attributes[kSmartEditInputType];
|
||||
|
||||
final focusNode = FocusNode();
|
||||
final client = http.Client();
|
||||
dartz.Either<OpenAIError, TextEditResponse>? result;
|
||||
bool loading = true;
|
||||
String result = '';
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -72,12 +154,7 @@ class _SmartEditInputState extends State<_SmartEditInput> {
|
||||
widget.editorState.service.keyboardService?.enable();
|
||||
}
|
||||
});
|
||||
_requestEdits().then(
|
||||
(value) => setState(() {
|
||||
result = value;
|
||||
loading = false;
|
||||
}),
|
||||
);
|
||||
_requestCompletions();
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -99,28 +176,16 @@ class _SmartEditInputState extends State<_SmartEditInput> {
|
||||
}
|
||||
|
||||
Widget _buildSmartEditPanel(BuildContext context) {
|
||||
return RawKeyboardListener(
|
||||
focusNode: focusNode,
|
||||
onKey: (RawKeyEvent event) async {
|
||||
if (event is! RawKeyDownEvent) return;
|
||||
if (event.logicalKey == LogicalKeyboardKey.enter) {
|
||||
await _onReplace();
|
||||
await _onExit();
|
||||
} else if (event.logicalKey == LogicalKeyboardKey.escape) {
|
||||
await _onExit();
|
||||
}
|
||||
},
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildHeaderWidget(context),
|
||||
const Space(0, 10),
|
||||
_buildResultWidget(context),
|
||||
const Space(0, 10),
|
||||
_buildInputFooterWidget(context),
|
||||
],
|
||||
),
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildHeaderWidget(context),
|
||||
const Space(0, 10),
|
||||
_buildResultWidget(context),
|
||||
const Space(0, 10),
|
||||
_buildInputFooterWidget(context),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -128,41 +193,39 @@ class _SmartEditInputState extends State<_SmartEditInput> {
|
||||
return Row(
|
||||
children: [
|
||||
FlowyText.medium(
|
||||
LocaleKeys.document_plugins_smartEditTitleName.tr(),
|
||||
'${LocaleKeys.document_plugins_openAI.tr()}: ${action.name}',
|
||||
fontSize: 14,
|
||||
),
|
||||
const Spacer(),
|
||||
FlowyText.regular(
|
||||
LocaleKeys.document_plugins_autoGeneratorLearnMore.tr(),
|
||||
),
|
||||
FlowyButton(
|
||||
useIntrinsicWidth: true,
|
||||
text: FlowyText.regular(
|
||||
LocaleKeys.document_plugins_autoGeneratorLearnMore.tr(),
|
||||
),
|
||||
onTap: () async {
|
||||
await openLearnMorePage();
|
||||
},
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildResultWidget(BuildContext context) {
|
||||
final loading = SizedBox.fromSize(
|
||||
size: const Size.square(14),
|
||||
child: const CircularProgressIndicator(),
|
||||
final loading = Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4.0),
|
||||
child: SizedBox.fromSize(
|
||||
size: const Size.square(14),
|
||||
child: const CircularProgressIndicator(),
|
||||
),
|
||||
);
|
||||
if (result == null) {
|
||||
if (result.isEmpty) {
|
||||
return loading;
|
||||
}
|
||||
return result!.fold((error) {
|
||||
return Flexible(
|
||||
child: Text(
|
||||
error.message,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Colors.red,
|
||||
),
|
||||
),
|
||||
);
|
||||
}, (response) {
|
||||
return Flexible(
|
||||
child: Text(
|
||||
response.choices.map((e) => e.text).join('\n'),
|
||||
),
|
||||
);
|
||||
});
|
||||
return Flexible(
|
||||
child: Text(
|
||||
result,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildInputFooterWidget(BuildContext context) {
|
||||
@@ -172,19 +235,13 @@ class _SmartEditInputState extends State<_SmartEditInput> {
|
||||
TextSpan(
|
||||
children: [
|
||||
TextSpan(
|
||||
text: '${LocaleKeys.button_replace.tr()} ',
|
||||
text: LocaleKeys.button_replace.tr(),
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
TextSpan(
|
||||
text: '↵',
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
onPressed: () {
|
||||
_onReplace();
|
||||
onPressed: () async {
|
||||
await _onReplace();
|
||||
_onExit();
|
||||
},
|
||||
),
|
||||
@@ -193,19 +250,33 @@ class _SmartEditInputState extends State<_SmartEditInput> {
|
||||
TextSpan(
|
||||
children: [
|
||||
TextSpan(
|
||||
text: '${LocaleKeys.button_Cancel.tr()} ',
|
||||
text: LocaleKeys.button_insertBelow.tr(),
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
],
|
||||
),
|
||||
onPressed: () async {
|
||||
await _onInsertBelow();
|
||||
_onExit();
|
||||
},
|
||||
),
|
||||
const Space(10, 0),
|
||||
FlowyRichTextButton(
|
||||
TextSpan(
|
||||
children: [
|
||||
TextSpan(
|
||||
text: LocaleKeys.button_esc.tr(),
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Colors.grey,
|
||||
),
|
||||
text: LocaleKeys.button_Cancel.tr(),
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
],
|
||||
),
|
||||
onPressed: () async => await _onExit(),
|
||||
),
|
||||
const Spacer(),
|
||||
FlowyText.regular(
|
||||
LocaleKeys.document_plugins_warning.tr(),
|
||||
color: Theme.of(context).hintColor,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -216,13 +287,11 @@ class _SmartEditInputState extends State<_SmartEditInput> {
|
||||
final selectedNodes = widget
|
||||
.editorState.service.selectionService.currentSelectedNodes.normalized
|
||||
.whereType<TextNode>();
|
||||
if (selection == null || result == null || result!.isLeft()) {
|
||||
if (selection == null || result.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
final texts = result!.asRight().choices.first.text.split('\n')
|
||||
..removeWhere((element) => element.isEmpty);
|
||||
assert(texts.length == selectedNodes.length);
|
||||
final texts = result.split('\n')..removeWhere((element) => element.isEmpty);
|
||||
final transaction = widget.editorState.transaction;
|
||||
transaction.replaceTexts(
|
||||
selectedNodes.toList(growable: false),
|
||||
@@ -232,6 +301,25 @@ class _SmartEditInputState extends State<_SmartEditInput> {
|
||||
return widget.editorState.apply(transaction);
|
||||
}
|
||||
|
||||
Future<void> _onInsertBelow() async {
|
||||
final selection = widget.editorState.service.selectionService
|
||||
.currentSelection.value?.normalized;
|
||||
if (selection == null || result.isEmpty) {
|
||||
return;
|
||||
}
|
||||
final texts = result.split('\n')..removeWhere((element) => element.isEmpty);
|
||||
final transaction = widget.editorState.transaction;
|
||||
transaction.insertNodes(
|
||||
selection.normalized.end.path.next,
|
||||
texts.map(
|
||||
(e) => TextNode(
|
||||
delta: Delta()..insert(e),
|
||||
),
|
||||
),
|
||||
);
|
||||
return widget.editorState.apply(transaction);
|
||||
}
|
||||
|
||||
Future<void> _onExit() async {
|
||||
final transaction = widget.editorState.transaction;
|
||||
transaction.deleteNode(widget.node);
|
||||
@@ -244,35 +332,62 @@ class _SmartEditInputState extends State<_SmartEditInput> {
|
||||
);
|
||||
}
|
||||
|
||||
Future<dartz.Either<OpenAIError, TextEditResponse>> _requestEdits() async {
|
||||
Future<void> _requestCompletions() async {
|
||||
final result = await UserBackendService.getCurrentUserProfile();
|
||||
return result.fold((userProfile) async {
|
||||
return result.fold((l) async {
|
||||
final openAIRepository = HttpOpenAIRepository(
|
||||
client: client,
|
||||
apiKey: userProfile.openaiKey,
|
||||
apiKey: l.openaiKey,
|
||||
);
|
||||
final edits = await openAIRepository.getEdits(
|
||||
input: input,
|
||||
instruction: instruction,
|
||||
n: input.split('\n').length,
|
||||
);
|
||||
return edits.fold((error) async {
|
||||
return dartz.Left(
|
||||
OpenAIError(
|
||||
message:
|
||||
LocaleKeys.document_plugins_smartEditCouldNotFetchResult.tr(),
|
||||
),
|
||||
var lines = input.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(() {
|
||||
this.result += response.choices.first.text;
|
||||
});
|
||||
},
|
||||
onEnd: () async {
|
||||
setState(() {
|
||||
if (i != lines.length - 1) {
|
||||
this.result += '\n';
|
||||
}
|
||||
});
|
||||
},
|
||||
onError: (error) async {
|
||||
await _showError(error.message);
|
||||
await _onExit();
|
||||
},
|
||||
);
|
||||
}, (textEdit) async {
|
||||
return dartz.Right(textEdit);
|
||||
});
|
||||
}, (error) async {
|
||||
// error
|
||||
return dartz.Left(
|
||||
OpenAIError(
|
||||
message: LocaleKeys.document_plugins_smartEditCouldNotFetchKey.tr(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}, (r) async {
|
||||
await _showError(r.msg);
|
||||
await _onExit();
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _showError(String message) async {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
action: SnackBarAction(
|
||||
label: LocaleKeys.button_Cancel.tr(),
|
||||
onPressed: () {
|
||||
ScaffoldMessenger.of(context).hideCurrentSnackBar();
|
||||
},
|
||||
),
|
||||
content: FlowyText(message),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+45
-4
@@ -1,10 +1,14 @@
|
||||
import 'package:appflowy/plugins/document/presentation/plugins/openai/widgets/smart_edit_action.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/plugins/openai/widgets/smart_edit_node_widget.dart';
|
||||
import 'package:appflowy/user/application/user_service.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/icon_button.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
|
||||
ToolbarItem smartEditItem = ToolbarItem(
|
||||
id: 'appflowy.toolbar.smart_edit',
|
||||
@@ -33,6 +37,20 @@ class _SmartEditWidget extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _SmartEditWidgetState extends State<_SmartEditWidget> {
|
||||
bool isOpenAIEnabled = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
UserBackendService.getCurrentUserProfile().then((value) {
|
||||
setState(() {
|
||||
isOpenAIEnabled =
|
||||
value.fold((l) => l.openaiKey.isNotEmpty, (r) => false);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return PopoverActionList<SmartEditActionWrapper>(
|
||||
@@ -43,7 +61,9 @@ class _SmartEditWidgetState extends State<_SmartEditWidget> {
|
||||
buildChild: (controller) {
|
||||
return FlowyIconButton(
|
||||
hoverColor: Colors.transparent,
|
||||
tooltipText: 'Smart Edit',
|
||||
tooltipText: isOpenAIEnabled
|
||||
? LocaleKeys.document_plugins_smartEdit.tr()
|
||||
: LocaleKeys.document_plugins_smartEditDisabled.tr(),
|
||||
preferBelow: false,
|
||||
icon: const Icon(
|
||||
Icons.lightbulb_outline,
|
||||
@@ -51,7 +71,11 @@ class _SmartEditWidgetState extends State<_SmartEditWidget> {
|
||||
color: Colors.white,
|
||||
),
|
||||
onPressed: () {
|
||||
controller.show();
|
||||
if (isOpenAIEnabled) {
|
||||
controller.show();
|
||||
} else {
|
||||
_showError(LocaleKeys.document_plugins_smartEditDisabled.tr());
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
@@ -77,14 +101,17 @@ class _SmartEditWidgetState extends State<_SmartEditWidget> {
|
||||
textNodes.normalized,
|
||||
selection.normalized,
|
||||
);
|
||||
while (input.last.isEmpty) {
|
||||
input.removeLast();
|
||||
}
|
||||
final transaction = widget.editorState.transaction;
|
||||
transaction.insertNode(
|
||||
selection.normalized.end.path.next,
|
||||
Node(
|
||||
type: kSmartEditType,
|
||||
attributes: {
|
||||
kSmartEditInstructionType: actionWrapper.inner.toInstruction,
|
||||
kSmartEditInputType: input,
|
||||
kSmartEditInstructionType: actionWrapper.inner.index,
|
||||
kSmartEditInputType: input.join('\n\n'),
|
||||
},
|
||||
),
|
||||
);
|
||||
@@ -97,4 +124,18 @@ class _SmartEditWidgetState extends State<_SmartEditWidget> {
|
||||
withUpdateCursor: false,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _showError(String message) async {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
action: SnackBarAction(
|
||||
label: LocaleKeys.button_Cancel.tr(),
|
||||
onPressed: () {
|
||||
ScaffoldMessenger.of(context).hideCurrentSnackBar();
|
||||
},
|
||||
),
|
||||
content: FlowyText(message),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import 'dart:io';
|
||||
|
||||
import 'package:appflowy_backend/appflowy_backend.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
|
||||
import '../startup.dart';
|
||||
|
||||
@@ -35,11 +36,11 @@ Future<Directory> appFlowyDocumentDirectory() async {
|
||||
switch (integrationEnv()) {
|
||||
case IntegrationMode.develop:
|
||||
Directory documentsDir = await getApplicationDocumentsDirectory();
|
||||
return Directory('${documentsDir.path}/flowy_dev').create();
|
||||
return Directory(path.join(documentsDir.path, 'flowy_dev')).create();
|
||||
case IntegrationMode.release:
|
||||
Directory documentsDir = await getApplicationDocumentsDirectory();
|
||||
return Directory('${documentsDir.path}/flowy').create();
|
||||
return Directory(path.join(documentsDir.path, 'flowy')).create();
|
||||
case IntegrationMode.test:
|
||||
return Directory("${Directory.current.path}/.sandbox");
|
||||
return Directory(path.join(Directory.current.path, '.sandbox'));
|
||||
}
|
||||
}
|
||||
@@ -83,11 +83,10 @@ class UserWorkspaceListener {
|
||||
PublishNotifier();
|
||||
|
||||
FolderNotificationListener? _listener;
|
||||
final UserProfilePB _userProfile;
|
||||
|
||||
UserWorkspaceListener({
|
||||
required UserProfilePB userProfile,
|
||||
}) : _userProfile = userProfile;
|
||||
});
|
||||
|
||||
void start({
|
||||
void Function(AuthNotifyValue)? onAuthChanged,
|
||||
@@ -106,14 +105,18 @@ class UserWorkspaceListener {
|
||||
_settingChangedNotifier?.addPublishListener(onSettingUpdated);
|
||||
}
|
||||
|
||||
// The "current-workspace" is predefined in the backend. Do not try to
|
||||
// modify it
|
||||
_listener = FolderNotificationListener(
|
||||
objectId: _userProfile.token,
|
||||
objectId: "current-workspace",
|
||||
handler: _handleObservableType,
|
||||
);
|
||||
}
|
||||
|
||||
void _handleObservableType(
|
||||
FolderNotification ty, Either<Uint8List, FlowyError> result) {
|
||||
FolderNotification ty,
|
||||
Either<Uint8List, FlowyError> result,
|
||||
) {
|
||||
switch (ty) {
|
||||
case FolderNotification.DidCreateWorkspace:
|
||||
case FolderNotification.DidDeleteWorkspace:
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class Debounce {
|
||||
final Duration duration;
|
||||
Timer? _timer;
|
||||
|
||||
Debounce({
|
||||
this.duration = const Duration(milliseconds: 1000),
|
||||
});
|
||||
|
||||
void call(VoidCallback action) {
|
||||
dispose();
|
||||
_timer = Timer(duration, () {
|
||||
action();
|
||||
});
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
_timer?.cancel();
|
||||
_timer = null;
|
||||
}
|
||||
}
|
||||
+23
-5
@@ -1,4 +1,5 @@
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/util/debounce.dart';
|
||||
import 'package:flowy_infra/size.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -98,11 +99,20 @@ class _OpenaiKeyInput extends StatefulWidget {
|
||||
|
||||
class _OpenaiKeyInputState extends State<_OpenaiKeyInput> {
|
||||
bool visible = false;
|
||||
final textEditingController = TextEditingController();
|
||||
final debounce = Debounce();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
textEditingController.text = widget.openAIKey;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return TextField(
|
||||
controller: TextEditingController()..text = widget.openAIKey,
|
||||
controller: textEditingController,
|
||||
obscureText: !visible,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'OpenAI Key',
|
||||
@@ -120,13 +130,21 @@ class _OpenaiKeyInputState extends State<_OpenaiKeyInput> {
|
||||
},
|
||||
),
|
||||
),
|
||||
onSubmitted: (val) {
|
||||
context
|
||||
.read<SettingsUserViewBloc>()
|
||||
.add(SettingsUserEvent.updateUserOpenAIKey(val));
|
||||
onChanged: (value) {
|
||||
debounce.call(() {
|
||||
context
|
||||
.read<SettingsUserViewBloc>()
|
||||
.add(SettingsUserEvent.updateUserOpenAIKey(value));
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
debounce.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
class _CurrentIcon extends StatelessWidget {
|
||||
|
||||
+1
-2
@@ -134,7 +134,6 @@ class FlowyVersionDescription extends CustomActionCell {
|
||||
PackageInfo packageInfo = snapshot.data;
|
||||
String appName = packageInfo.appName;
|
||||
String version = packageInfo.version;
|
||||
String buildNumber = packageInfo.buildNumber;
|
||||
|
||||
return SizedBox(
|
||||
height: 30,
|
||||
@@ -148,7 +147,7 @@ class FlowyVersionDescription extends CustomActionCell {
|
||||
thickness: 1.0),
|
||||
const VSpace(6),
|
||||
FlowyText(
|
||||
"$appName $version.$buildNumber",
|
||||
"$appName $version",
|
||||
color: Theme.of(context).hintColor,
|
||||
),
|
||||
],
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
cmake_minimum_required(VERSION 3.10)
|
||||
project(runner LANGUAGES CXX)
|
||||
|
||||
set(BINARY_NAME "appflowy_flutter")
|
||||
set(APPLICATION_ID "com.example.appflowy_flutter")
|
||||
set(BINARY_NAME "AppFlowy")
|
||||
set(APPLICATION_ID "io.appflowy.appflowy")
|
||||
|
||||
cmake_policy(SET CMP0063 NEW)
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
Name=AppFlowy
|
||||
Comment=An Open Source Alternative to Notion
|
||||
Icon=[CHANGE_THIS]/AppFlowy/flowy_logo.svg
|
||||
Exec=[CHANGE_THIS]/AppFlowy/appflowy_flutter
|
||||
Exec=[CHANGE_THIS]/AppFlowy/AppFlowy
|
||||
Categories=Office
|
||||
Type=Application
|
||||
Terminal=false
|
||||
@@ -7,17 +7,19 @@
|
||||
|
||||
#include "flutter/generated_plugin_registrant.h"
|
||||
|
||||
struct _MyApplication {
|
||||
struct _MyApplication
|
||||
{
|
||||
GtkApplication parent_instance;
|
||||
char** dart_entrypoint_arguments;
|
||||
char **dart_entrypoint_arguments;
|
||||
};
|
||||
|
||||
G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION)
|
||||
|
||||
// Implements GApplication::activate.
|
||||
static void my_application_activate(GApplication* application) {
|
||||
MyApplication* self = MY_APPLICATION(application);
|
||||
GtkWindow* window =
|
||||
static void my_application_activate(GApplication *application)
|
||||
{
|
||||
MyApplication *self = MY_APPLICATION(application);
|
||||
GtkWindow *window =
|
||||
GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application)));
|
||||
|
||||
// Use a header bar when running in GNOME as this is the common style used
|
||||
@@ -29,22 +31,27 @@ static void my_application_activate(GApplication* application) {
|
||||
// if future cases occur).
|
||||
gboolean use_header_bar = TRUE;
|
||||
#ifdef GDK_WINDOWING_X11
|
||||
GdkScreen* screen = gtk_window_get_screen(window);
|
||||
if (GDK_IS_X11_SCREEN(screen)) {
|
||||
const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen);
|
||||
if (g_strcmp0(wm_name, "GNOME Shell") != 0) {
|
||||
GdkScreen *screen = gtk_window_get_screen(window);
|
||||
if (GDK_IS_X11_SCREEN(screen))
|
||||
{
|
||||
const gchar *wm_name = gdk_x11_screen_get_window_manager_name(screen);
|
||||
if (g_strcmp0(wm_name, "GNOME Shell") != 0)
|
||||
{
|
||||
use_header_bar = FALSE;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
if (use_header_bar) {
|
||||
GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new());
|
||||
if (use_header_bar)
|
||||
{
|
||||
GtkHeaderBar *header_bar = GTK_HEADER_BAR(gtk_header_bar_new());
|
||||
gtk_widget_show(GTK_WIDGET(header_bar));
|
||||
gtk_header_bar_set_title(header_bar, "appflowy_flutter");
|
||||
gtk_header_bar_set_title(header_bar, "AppFlowy");
|
||||
gtk_header_bar_set_show_close_button(header_bar, TRUE);
|
||||
gtk_window_set_titlebar(window, GTK_WIDGET(header_bar));
|
||||
} else {
|
||||
gtk_window_set_title(window, "appflowy_flutter");
|
||||
}
|
||||
else
|
||||
{
|
||||
gtk_window_set_title(window, "AppFlowy");
|
||||
}
|
||||
|
||||
gtk_window_set_default_size(window, 1280, 720);
|
||||
@@ -53,7 +60,7 @@ static void my_application_activate(GApplication* application) {
|
||||
g_autoptr(FlDartProject) project = fl_dart_project_new();
|
||||
fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments);
|
||||
|
||||
FlView* view = fl_view_new(project);
|
||||
FlView *view = fl_view_new(project);
|
||||
gtk_widget_show(GTK_WIDGET(view));
|
||||
gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view));
|
||||
|
||||
@@ -63,16 +70,18 @@ static void my_application_activate(GApplication* application) {
|
||||
}
|
||||
|
||||
// Implements GApplication::local_command_line.
|
||||
static gboolean my_application_local_command_line(GApplication* application, gchar*** arguments, int* exit_status) {
|
||||
MyApplication* self = MY_APPLICATION(application);
|
||||
static gboolean my_application_local_command_line(GApplication *application, gchar ***arguments, int *exit_status)
|
||||
{
|
||||
MyApplication *self = MY_APPLICATION(application);
|
||||
// Strip out the first argument as it is the binary name.
|
||||
self->dart_entrypoint_arguments = g_strdupv(*arguments + 1);
|
||||
|
||||
g_autoptr(GError) error = nullptr;
|
||||
if (!g_application_register(application, nullptr, &error)) {
|
||||
g_warning("Failed to register: %s", error->message);
|
||||
*exit_status = 1;
|
||||
return TRUE;
|
||||
if (!g_application_register(application, nullptr, &error))
|
||||
{
|
||||
g_warning("Failed to register: %s", error->message);
|
||||
*exit_status = 1;
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
g_application_activate(application);
|
||||
@@ -82,21 +91,24 @@ static gboolean my_application_local_command_line(GApplication* application, gch
|
||||
}
|
||||
|
||||
// Implements GObject::dispose.
|
||||
static void my_application_dispose(GObject* object) {
|
||||
MyApplication* self = MY_APPLICATION(object);
|
||||
static void my_application_dispose(GObject *object)
|
||||
{
|
||||
MyApplication *self = MY_APPLICATION(object);
|
||||
g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev);
|
||||
G_OBJECT_CLASS(my_application_parent_class)->dispose(object);
|
||||
}
|
||||
|
||||
static void my_application_class_init(MyApplicationClass* klass) {
|
||||
static void my_application_class_init(MyApplicationClass *klass)
|
||||
{
|
||||
G_APPLICATION_CLASS(klass)->activate = my_application_activate;
|
||||
G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line;
|
||||
G_OBJECT_CLASS(klass)->dispose = my_application_dispose;
|
||||
}
|
||||
|
||||
static void my_application_init(MyApplication* self) {}
|
||||
static void my_application_init(MyApplication *self) {}
|
||||
|
||||
MyApplication* my_application_new() {
|
||||
MyApplication *my_application_new()
|
||||
{
|
||||
return MY_APPLICATION(g_object_new(my_application_get_type(),
|
||||
"application-id", APPLICATION_ID,
|
||||
"flags", G_APPLICATION_NON_UNIQUE,
|
||||
|
||||
@@ -5,10 +5,10 @@
|
||||
// 'flutter create' template.
|
||||
|
||||
// The application's name. By default this is also the title of the Flutter window.
|
||||
PRODUCT_NAME = appflowy_flutter
|
||||
PRODUCT_NAME = AppFlowy
|
||||
|
||||
// The application's bundle identifier
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.appFlowy
|
||||
PRODUCT_BUNDLE_IDENTIFIER = io.appflowy.appflowy
|
||||
|
||||
// The copyright displayed in application information
|
||||
PRODUCT_COPYRIGHT = Copyright © 2021 com.example. All rights reserved.
|
||||
PRODUCT_COPYRIGHT = Copyright © 2023 AppFlowy.IO. All rights reserved.
|
||||
+6
-3
@@ -52,12 +52,15 @@ extension CommandExtension on EditorState {
|
||||
throw Exception('path and textNode cannot be null at the same time');
|
||||
}
|
||||
|
||||
String getTextInSelection(
|
||||
List<String> getTextInSelection(
|
||||
List<TextNode> textNodes,
|
||||
Selection selection,
|
||||
) {
|
||||
List<String> res = [];
|
||||
if (!selection.isCollapsed) {
|
||||
if (selection.isSingle) {
|
||||
final plainText = textNodes.first.toPlainText();
|
||||
res.add(plainText.substring(selection.startIndex, selection.endIndex));
|
||||
} else if (!selection.isCollapsed) {
|
||||
for (var i = 0; i < textNodes.length; i++) {
|
||||
final plainText = textNodes[i].toPlainText();
|
||||
if (i == 0) {
|
||||
@@ -74,6 +77,6 @@ extension CommandExtension on EditorState {
|
||||
}
|
||||
}
|
||||
}
|
||||
return res.join('\n');
|
||||
return res;
|
||||
}
|
||||
}
|
||||
+98
-21
@@ -264,11 +264,11 @@ extension TextTransaction on Transaction {
|
||||
if (index != 0 && attributes == null) {
|
||||
newAttributes =
|
||||
textNode.delta.slice(max(index - 1, 0), index).first.attributes;
|
||||
if (newAttributes != null) {
|
||||
newAttributes = {...newAttributes}; // make a copy
|
||||
} else {
|
||||
newAttributes =
|
||||
textNode.delta.slice(index, index + length).first.attributes;
|
||||
if (newAttributes == null) {
|
||||
final slicedDelta = textNode.delta.slice(index, index + length);
|
||||
if (slicedDelta.isNotEmpty) {
|
||||
newAttributes = slicedDelta.first.attributes;
|
||||
}
|
||||
}
|
||||
}
|
||||
updateText(
|
||||
@@ -276,7 +276,7 @@ extension TextTransaction on Transaction {
|
||||
Delta()
|
||||
..retain(index)
|
||||
..delete(length)
|
||||
..insert(text, attributes: newAttributes),
|
||||
..insert(text, attributes: {...newAttributes ?? {}}),
|
||||
);
|
||||
afterSelection = Selection.collapsed(
|
||||
Position(
|
||||
@@ -291,46 +291,123 @@ extension TextTransaction on Transaction {
|
||||
Selection selection,
|
||||
List<String> texts,
|
||||
) {
|
||||
if (textNodes.isEmpty) {
|
||||
if (textNodes.isEmpty || texts.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (selection.isSingle) {
|
||||
assert(textNodes.length == 1 && texts.length == 1);
|
||||
replaceText(
|
||||
textNodes.first,
|
||||
selection.startIndex,
|
||||
selection.length,
|
||||
texts.first,
|
||||
);
|
||||
} else {
|
||||
if (textNodes.length == texts.length) {
|
||||
final length = textNodes.length;
|
||||
for (var i = 0; i < length; i++) {
|
||||
|
||||
if (length == 1) {
|
||||
replaceText(
|
||||
textNodes.first,
|
||||
selection.startIndex,
|
||||
selection.endIndex - selection.startIndex,
|
||||
texts.first,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
for (var i = 0; i < textNodes.length; i++) {
|
||||
final textNode = textNodes[i];
|
||||
final text = texts[i];
|
||||
if (i == 0) {
|
||||
replaceText(
|
||||
textNode,
|
||||
selection.startIndex,
|
||||
textNode.toPlainText().length,
|
||||
text,
|
||||
texts.first,
|
||||
);
|
||||
} else if (i == length - 1) {
|
||||
replaceText(
|
||||
textNode,
|
||||
0,
|
||||
selection.endIndex,
|
||||
text,
|
||||
texts.last,
|
||||
);
|
||||
} else {
|
||||
replaceText(
|
||||
textNode,
|
||||
0,
|
||||
textNode.toPlainText().length,
|
||||
text,
|
||||
texts[i],
|
||||
);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (textNodes.length > texts.length) {
|
||||
final length = textNodes.length;
|
||||
for (var i = 0; i < textNodes.length; i++) {
|
||||
final textNode = textNodes[i];
|
||||
if (i == 0) {
|
||||
replaceText(
|
||||
textNode,
|
||||
selection.startIndex,
|
||||
textNode.toPlainText().length,
|
||||
texts.first,
|
||||
);
|
||||
} else if (i == length - 1 && texts.length >= 2) {
|
||||
replaceText(
|
||||
textNode,
|
||||
0,
|
||||
selection.endIndex,
|
||||
texts.last,
|
||||
);
|
||||
} else if (i < texts.length - 1) {
|
||||
replaceText(
|
||||
textNode,
|
||||
0,
|
||||
textNode.toPlainText().length,
|
||||
texts[i],
|
||||
);
|
||||
} else {
|
||||
deleteNode(textNode);
|
||||
}
|
||||
}
|
||||
afterSelection = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (textNodes.length < texts.length) {
|
||||
final length = texts.length;
|
||||
for (var i = 0; i < texts.length; i++) {
|
||||
final text = texts[i];
|
||||
if (i == 0) {
|
||||
replaceText(
|
||||
textNodes.first,
|
||||
selection.startIndex,
|
||||
textNodes.first.toPlainText().length,
|
||||
text,
|
||||
);
|
||||
} else if (i == length - 1) {
|
||||
replaceText(
|
||||
textNodes.last,
|
||||
0,
|
||||
selection.endIndex,
|
||||
text,
|
||||
);
|
||||
} else {
|
||||
if (i < textNodes.length - 1) {
|
||||
replaceText(
|
||||
textNodes[i],
|
||||
0,
|
||||
textNodes[i].toPlainText().length,
|
||||
text,
|
||||
);
|
||||
} else {
|
||||
var path = textNodes.first.path;
|
||||
var j = i - textNodes.length + length - 1;
|
||||
while (j > 0) {
|
||||
path = path.next;
|
||||
j--;
|
||||
}
|
||||
insertNode(path, TextNode(delta: Delta()..insert(text)));
|
||||
}
|
||||
}
|
||||
}
|
||||
afterSelection = null;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
-1
@@ -8,7 +8,9 @@ ShortcutEventHandler selectAllHandler = (editorState, event) {
|
||||
if (editorState.document.root.children.isEmpty) {
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
final firstNode = editorState.document.root.children.first;
|
||||
final firstNode = editorState.document.root.children.firstWhere(
|
||||
(element) => element is TextNode,
|
||||
);
|
||||
final lastNode = editorState.document.root.children.last;
|
||||
var offset = 0;
|
||||
if (lastNode is TextNode) {
|
||||
|
||||
+1
@@ -112,6 +112,7 @@ class _AppFlowyKeyboardState extends State<AppFlowyKeyboard>
|
||||
isFocus = false;
|
||||
this.showCursor = showCursor;
|
||||
_focusNode.unfocus(disposition: disposition);
|
||||
_onFocusChange(false);
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
+25
-1
@@ -82,6 +82,13 @@ abstract class AppFlowySelectionService {
|
||||
|
||||
/// The current selection areas's rect in editor.
|
||||
List<Rect> get selectionRects;
|
||||
|
||||
void register(SelectionInterceptor interceptor);
|
||||
void unRegister(SelectionInterceptor interceptor);
|
||||
}
|
||||
|
||||
class SelectionInterceptor {
|
||||
bool Function(TapDownDetails details)? canTap;
|
||||
}
|
||||
|
||||
class AppFlowySelection extends StatefulWidget {
|
||||
@@ -212,6 +219,7 @@ class _AppFlowySelectionState extends State<AppFlowySelection>
|
||||
|
||||
selectionRects.clear();
|
||||
clearSelection();
|
||||
_clearToolbar();
|
||||
|
||||
if (selection != null) {
|
||||
if (selection.isCollapsed) {
|
||||
@@ -286,6 +294,10 @@ class _AppFlowySelectionState extends State<AppFlowySelection>
|
||||
}
|
||||
|
||||
void _onTapDown(TapDownDetails details) {
|
||||
final canTap =
|
||||
_interceptors.every((element) => element.canTap?.call(details) ?? true);
|
||||
if (!canTap) return;
|
||||
|
||||
// clear old state.
|
||||
_panStartOffset = null;
|
||||
|
||||
@@ -347,8 +359,9 @@ class _AppFlowySelectionState extends State<AppFlowySelection>
|
||||
|
||||
void _onPanStart(DragStartDetails details) {
|
||||
clearSelection();
|
||||
_clearToolbar();
|
||||
|
||||
_panStartOffset = details.globalPosition;
|
||||
_panStartOffset = details.globalPosition.translate(-3.0, 0);
|
||||
_panStartScrollDy = editorState.service.scrollService?.dy;
|
||||
|
||||
_enableInteraction();
|
||||
@@ -700,4 +713,15 @@ class _AppFlowySelectionState extends State<AppFlowySelection>
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
final List<SelectionInterceptor> _interceptors = [];
|
||||
@override
|
||||
void register(SelectionInterceptor interceptor) {
|
||||
_interceptors.add(interceptor);
|
||||
}
|
||||
|
||||
@override
|
||||
void unRegister(SelectionInterceptor interceptor) {
|
||||
_interceptors.removeWhere((element) => element == interceptor);
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -26,11 +26,11 @@ void main() {
|
||||
.editorState.service.selectionService.currentSelectedNodes
|
||||
.whereType<TextNode>()
|
||||
.toList(growable: false);
|
||||
final text = editor.editorState.getTextInSelection(
|
||||
final texts = editor.editorState.getTextInSelection(
|
||||
textNodes.normalized,
|
||||
selection.normalized,
|
||||
);
|
||||
expect(text, 'me\nto\nAppfl');
|
||||
expect(texts, ['me', 'to', 'Appfl']);
|
||||
});
|
||||
});
|
||||
}
|
||||
+169
@@ -0,0 +1,169 @@
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import '../../infra/test_editor.dart';
|
||||
|
||||
Document createEmptyDocument() {
|
||||
return Document(
|
||||
root: Node(
|
||||
type: 'editor',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void main() async {
|
||||
group('transaction.dart', () {
|
||||
testWidgets('test replaceTexts, textNodes.length == texts.length',
|
||||
(tester) async {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
final editor = tester.editor
|
||||
..insertTextNode('0123456789')
|
||||
..insertTextNode('0123456789')
|
||||
..insertTextNode('0123456789')
|
||||
..insertTextNode('0123456789');
|
||||
await editor.startTesting();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(editor.documentLength, 4);
|
||||
|
||||
final selection = Selection(
|
||||
start: Position(path: [0], offset: 4),
|
||||
end: Position(path: [3], offset: 4),
|
||||
);
|
||||
final transaction = editor.editorState.transaction;
|
||||
var textNodes = [0, 1, 2, 3]
|
||||
.map((e) => editor.nodeAtPath([e])!)
|
||||
.whereType<TextNode>()
|
||||
.toList(growable: false);
|
||||
final texts = ['ABC', 'ABC', 'ABC', 'ABC'];
|
||||
transaction.replaceTexts(textNodes, selection, texts);
|
||||
editor.editorState.apply(transaction);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(editor.documentLength, 4);
|
||||
textNodes = [0, 1, 2, 3]
|
||||
.map((e) => editor.nodeAtPath([e])!)
|
||||
.whereType<TextNode>()
|
||||
.toList(growable: false);
|
||||
expect(textNodes[0].toPlainText(), '0123ABC');
|
||||
expect(textNodes[1].toPlainText(), 'ABC');
|
||||
expect(textNodes[2].toPlainText(), 'ABC');
|
||||
expect(textNodes[3].toPlainText(), 'ABC456789');
|
||||
});
|
||||
|
||||
testWidgets('test replaceTexts, textNodes.length > texts.length',
|
||||
(tester) async {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
final editor = tester.editor
|
||||
..insertTextNode('0123456789')
|
||||
..insertTextNode('0123456789')
|
||||
..insertTextNode('0123456789')
|
||||
..insertTextNode('0123456789')
|
||||
..insertTextNode('0123456789');
|
||||
await editor.startTesting();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(editor.documentLength, 5);
|
||||
|
||||
final selection = Selection(
|
||||
start: Position(path: [0], offset: 4),
|
||||
end: Position(path: [4], offset: 4),
|
||||
);
|
||||
final transaction = editor.editorState.transaction;
|
||||
var textNodes = [0, 1, 2, 3, 4]
|
||||
.map((e) => editor.nodeAtPath([e])!)
|
||||
.whereType<TextNode>()
|
||||
.toList(growable: false);
|
||||
final texts = ['ABC', 'ABC', 'ABC', 'ABC'];
|
||||
transaction.replaceTexts(textNodes, selection, texts);
|
||||
editor.editorState.apply(transaction);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(editor.documentLength, 4);
|
||||
textNodes = [0, 1, 2, 3]
|
||||
.map((e) => editor.nodeAtPath([e])!)
|
||||
.whereType<TextNode>()
|
||||
.toList(growable: false);
|
||||
expect(textNodes[0].toPlainText(), '0123ABC');
|
||||
expect(textNodes[1].toPlainText(), 'ABC');
|
||||
expect(textNodes[2].toPlainText(), 'ABC');
|
||||
expect(textNodes[3].toPlainText(), 'ABC456789');
|
||||
});
|
||||
|
||||
testWidgets('test replaceTexts, textNodes.length >> texts.length',
|
||||
(tester) async {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
final editor = tester.editor
|
||||
..insertTextNode('0123456789')
|
||||
..insertTextNode('0123456789')
|
||||
..insertTextNode('0123456789')
|
||||
..insertTextNode('0123456789')
|
||||
..insertTextNode('0123456789');
|
||||
await editor.startTesting();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(editor.documentLength, 5);
|
||||
|
||||
final selection = Selection(
|
||||
start: Position(path: [0], offset: 4),
|
||||
end: Position(path: [4], offset: 4),
|
||||
);
|
||||
final transaction = editor.editorState.transaction;
|
||||
var textNodes = [0, 1, 2, 3, 4]
|
||||
.map((e) => editor.nodeAtPath([e])!)
|
||||
.whereType<TextNode>()
|
||||
.toList(growable: false);
|
||||
final texts = ['ABC'];
|
||||
transaction.replaceTexts(textNodes, selection, texts);
|
||||
editor.editorState.apply(transaction);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(editor.documentLength, 1);
|
||||
textNodes = [0]
|
||||
.map((e) => editor.nodeAtPath([e])!)
|
||||
.whereType<TextNode>()
|
||||
.toList(growable: false);
|
||||
expect(textNodes[0].toPlainText(), '0123ABC');
|
||||
});
|
||||
|
||||
testWidgets('test replaceTexts, textNodes.length < texts.length',
|
||||
(tester) async {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
final editor = tester.editor
|
||||
..insertTextNode('0123456789')
|
||||
..insertTextNode('0123456789')
|
||||
..insertTextNode('0123456789');
|
||||
await editor.startTesting();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(editor.documentLength, 3);
|
||||
|
||||
final selection = Selection(
|
||||
start: Position(path: [0], offset: 4),
|
||||
end: Position(path: [2], offset: 4),
|
||||
);
|
||||
final transaction = editor.editorState.transaction;
|
||||
var textNodes = [0, 1, 2]
|
||||
.map((e) => editor.nodeAtPath([e])!)
|
||||
.whereType<TextNode>()
|
||||
.toList(growable: false);
|
||||
final texts = ['ABC', 'ABC', 'ABC', 'ABC'];
|
||||
transaction.replaceTexts(textNodes, selection, texts);
|
||||
editor.editorState.apply(transaction);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(editor.documentLength, 4);
|
||||
textNodes = [0, 1, 2, 3]
|
||||
.map((e) => editor.nodeAtPath([e])!)
|
||||
.whereType<TextNode>()
|
||||
.toList(growable: false);
|
||||
expect(textNodes[0].toPlainText(), '0123ABC');
|
||||
expect(textNodes[1].toPlainText(), 'ABC');
|
||||
expect(textNodes[2].toPlainText(), 'ABC');
|
||||
expect(textNodes[3].toPlainText(), 'ABC456789');
|
||||
});
|
||||
});
|
||||
}
|
||||
+35
-1
@@ -10,7 +10,8 @@ void main() async {
|
||||
});
|
||||
|
||||
group('slash_handler.dart', () {
|
||||
testWidgets('Presses / to trigger selection menu', (tester) async {
|
||||
testWidgets('Presses / to trigger selection menu in 0 index',
|
||||
(tester) async {
|
||||
const text = 'Welcome to Appflowy 😁';
|
||||
const lines = 3;
|
||||
final editor = tester.editor;
|
||||
@@ -41,5 +42,38 @@ void main() async {
|
||||
findsNothing,
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('Presses / to trigger selection menu in not 0 index',
|
||||
(tester) async {
|
||||
const text = 'Welcome to Appflowy 😁';
|
||||
const lines = 3;
|
||||
final editor = tester.editor;
|
||||
for (var i = 0; i < lines; i++) {
|
||||
editor.insertTextNode(text);
|
||||
}
|
||||
await editor.startTesting();
|
||||
await editor.updateSelection(Selection.single(path: [1], startOffset: 5));
|
||||
await editor.pressLogicKey(LogicalKeyboardKey.slash);
|
||||
|
||||
await tester.pumpAndSettle(const Duration(milliseconds: 1000));
|
||||
|
||||
expect(
|
||||
find.byType(SelectionMenuWidget, skipOffstage: false),
|
||||
findsOneWidget,
|
||||
);
|
||||
|
||||
for (final item in defaultSelectionMenuItems) {
|
||||
expect(find.text(item.name), findsOneWidget);
|
||||
}
|
||||
|
||||
await editor.updateSelection(Selection.single(path: [1], startOffset: 0));
|
||||
|
||||
await tester.pumpAndSettle(const Duration(milliseconds: 200));
|
||||
|
||||
expect(
|
||||
find.byType(SelectionMenuItemWidget, skipOffstage: false),
|
||||
findsNothing,
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
+3
@@ -94,6 +94,7 @@ void main() async {
|
||||
await editor.updateSelection(
|
||||
Selection.single(path: [1], startOffset: 0, endOffset: text.length * 2),
|
||||
);
|
||||
await tester.pumpAndSettle(const Duration(milliseconds: 500));
|
||||
testHighlight(false);
|
||||
|
||||
await editor.updateSelection(
|
||||
@@ -103,6 +104,7 @@ void main() async {
|
||||
endOffset: text.length * 2,
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle(const Duration(milliseconds: 500));
|
||||
testHighlight(true);
|
||||
|
||||
await editor.updateSelection(
|
||||
@@ -112,6 +114,7 @@ void main() async {
|
||||
endOffset: text.length * 2 - 2,
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle(const Duration(milliseconds: 500));
|
||||
testHighlight(true);
|
||||
});
|
||||
|
||||
|
||||
@@ -72,6 +72,7 @@ class Popover extends StatefulWidget {
|
||||
final PopoverDirection direction;
|
||||
|
||||
final void Function()? onClose;
|
||||
final Future<bool> Function()? canClose;
|
||||
|
||||
final bool asBarrier;
|
||||
|
||||
@@ -92,6 +93,7 @@ class Popover extends StatefulWidget {
|
||||
this.mutex,
|
||||
this.windowPadding,
|
||||
this.onClose,
|
||||
this.canClose,
|
||||
this.asBarrier = false,
|
||||
}) : super(key: key);
|
||||
|
||||
@@ -122,7 +124,12 @@ class PopoverState extends State<Popover> {
|
||||
children.add(
|
||||
PopoverMask(
|
||||
decoration: widget.maskDecoration,
|
||||
onTap: () => _removeRootOverlay(),
|
||||
onTap: () async {
|
||||
if (!(await widget.canClose?.call() ?? true)) {
|
||||
return;
|
||||
}
|
||||
_removeRootOverlay();
|
||||
},
|
||||
onExit: () => _removeRootOverlay(),
|
||||
),
|
||||
);
|
||||
|
||||
+14
-4
@@ -10,11 +10,13 @@ class AppFlowyPopover extends StatelessWidget {
|
||||
final int triggerActions;
|
||||
final BoxConstraints constraints;
|
||||
final void Function()? onClose;
|
||||
final Future<bool> Function()? canClose;
|
||||
final PopoverMutex? mutex;
|
||||
final Offset? offset;
|
||||
final bool asBarrier;
|
||||
final EdgeInsets margin;
|
||||
final EdgeInsets windowPadding;
|
||||
final Decoration? decoration;
|
||||
|
||||
const AppFlowyPopover({
|
||||
Key? key,
|
||||
@@ -22,6 +24,7 @@ class AppFlowyPopover extends StatelessWidget {
|
||||
required this.popupBuilder,
|
||||
this.direction = PopoverDirection.rightWithTopAligned,
|
||||
this.onClose,
|
||||
this.canClose,
|
||||
this.constraints = const BoxConstraints(maxWidth: 240, maxHeight: 600),
|
||||
this.mutex,
|
||||
this.triggerActions = PopoverTriggerFlags.click,
|
||||
@@ -30,6 +33,7 @@ class AppFlowyPopover extends StatelessWidget {
|
||||
this.asBarrier = false,
|
||||
this.margin = const EdgeInsets.all(6),
|
||||
this.windowPadding = const EdgeInsets.all(8.0),
|
||||
this.decoration,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
@@ -37,6 +41,7 @@ class AppFlowyPopover extends StatelessWidget {
|
||||
return Popover(
|
||||
controller: controller,
|
||||
onClose: onClose,
|
||||
canClose: canClose,
|
||||
direction: direction,
|
||||
mutex: mutex,
|
||||
asBarrier: asBarrier,
|
||||
@@ -49,6 +54,7 @@ class AppFlowyPopover extends StatelessWidget {
|
||||
return _PopoverContainer(
|
||||
constraints: constraints,
|
||||
margin: margin,
|
||||
decoration: decoration,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
@@ -61,19 +67,23 @@ class _PopoverContainer extends StatelessWidget {
|
||||
final Widget child;
|
||||
final BoxConstraints constraints;
|
||||
final EdgeInsets margin;
|
||||
final Decoration? decoration;
|
||||
|
||||
const _PopoverContainer({
|
||||
required this.child,
|
||||
required this.margin,
|
||||
required this.constraints,
|
||||
required this.decoration,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final decoration = FlowyDecoration.decoration(
|
||||
Theme.of(context).colorScheme.surface,
|
||||
Theme.of(context).colorScheme.shadow.withOpacity(0.15),
|
||||
);
|
||||
final decoration = this.decoration ??
|
||||
FlowyDecoration.decoration(
|
||||
Theme.of(context).colorScheme.surface,
|
||||
Theme.of(context).colorScheme.shadow.withOpacity(0.15),
|
||||
);
|
||||
|
||||
return Material(
|
||||
type: MaterialType.transparency,
|
||||
|
||||
@@ -830,7 +830,7 @@ packages:
|
||||
source: hosted
|
||||
version: "1.0.5"
|
||||
path:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: path
|
||||
url: "https://pub.dartlang.org"
|
||||
|
||||
@@ -15,7 +15,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
|
||||
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
|
||||
# Read more about iOS versioning at
|
||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||
version: 1.0.0+1
|
||||
version: 0.1.2
|
||||
|
||||
environment:
|
||||
sdk: ">=2.18.0 <3.0.0"
|
||||
@@ -95,6 +95,7 @@ dependencies:
|
||||
window_manager: ^0.3.0
|
||||
http: ^0.13.5
|
||||
json_annotation: ^4.7.0
|
||||
path: ^1.8.2
|
||||
|
||||
dev_dependencies:
|
||||
flutter_lints: ^2.0.1
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
cmake_minimum_required(VERSION 3.14)
|
||||
project(appflowy_flutter LANGUAGES CXX)
|
||||
|
||||
set(BINARY_NAME "appflowy_flutter")
|
||||
set(BINARY_NAME "AppFlowy")
|
||||
|
||||
cmake_policy(SET CMP0063 NEW)
|
||||
|
||||
@@ -9,6 +9,7 @@ set(CMAKE_INSTALL_RPATH "$ORIGIN/lib")
|
||||
|
||||
# Configure build options.
|
||||
get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG)
|
||||
|
||||
if(IS_MULTICONFIG)
|
||||
set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release"
|
||||
CACHE STRING "" FORCE)
|
||||
@@ -50,14 +51,15 @@ add_subdirectory("runner")
|
||||
# them to the application.
|
||||
include(flutter/generated_plugins.cmake)
|
||||
|
||||
|
||||
# === Installation ===
|
||||
# Support files are copied into place next to the executable, so that it can
|
||||
# run in place. This is done instead of making a separate bundle (as on Linux)
|
||||
# so that building and running from within Visual Studio will work.
|
||||
set(BUILD_BUNDLE_DIR "$<TARGET_FILE_DIR:${BINARY_NAME}>")
|
||||
|
||||
# Make the "install" step default, as it's required to run.
|
||||
set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1)
|
||||
|
||||
if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
|
||||
set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE)
|
||||
endif()
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
cmake_minimum_required(VERSION 3.14)
|
||||
project(runner LANGUAGES CXX)
|
||||
|
||||
# Define the application target. To change its name, change BINARY_NAME in the
|
||||
# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer
|
||||
# work.
|
||||
#
|
||||
# Any new source files that you add to the application should be added here.
|
||||
add_executable(${BINARY_NAME} WIN32
|
||||
"flutter_window.cpp"
|
||||
"main.cpp"
|
||||
@@ -10,13 +15,25 @@ add_executable(${BINARY_NAME} WIN32
|
||||
"Runner.rc"
|
||||
"runner.exe.manifest"
|
||||
)
|
||||
|
||||
# Apply the standard set of build settings. This can be removed for applications
|
||||
# that need different build settings.
|
||||
apply_standard_settings(${BINARY_NAME})
|
||||
|
||||
# Add preprocessor definitions for the build version.
|
||||
target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"")
|
||||
target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}")
|
||||
target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}")
|
||||
target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}")
|
||||
target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}")
|
||||
|
||||
# Disable Windows macros that collide with C++ standard library functions.
|
||||
target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX")
|
||||
|
||||
# Add dependency libraries and include directories. Add any application-specific
|
||||
# dependencies here.
|
||||
target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app)
|
||||
target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}")
|
||||
|
||||
# Run the Flutter tool portions of the build. This must not be removed.
|
||||
add_dependencies(${BINARY_NAME} flutter_assemble)
|
||||
|
||||
|
||||
# === Flutter Library ===
|
||||
#set(DART_FFI "${CMAKE_CURRENT_SOURCE_DIR}/dart_ffi/dart_ffi.dll")
|
||||
#set(DART_FFI ${DART_FFI} PARENT_SCOPE)
|
||||
@@ -60,14 +60,14 @@ IDI_APP_ICON ICON "resources\\app_icon.ico"
|
||||
// Version
|
||||
//
|
||||
|
||||
#ifdef FLUTTER_BUILD_NUMBER
|
||||
#define VERSION_AS_NUMBER FLUTTER_BUILD_NUMBER
|
||||
#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD)
|
||||
#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD
|
||||
#else
|
||||
#define VERSION_AS_NUMBER 1,0,0
|
||||
#define VERSION_AS_NUMBER 1,0,0,0
|
||||
#endif
|
||||
|
||||
#ifdef FLUTTER_BUILD_NAME
|
||||
#define VERSION_AS_STRING #FLUTTER_BUILD_NAME
|
||||
#if defined(FLUTTER_VERSION)
|
||||
#define VERSION_AS_STRING FLUTTER_VERSION
|
||||
#else
|
||||
#define VERSION_AS_STRING "1.0.0"
|
||||
#endif
|
||||
@@ -89,13 +89,13 @@ BEGIN
|
||||
BEGIN
|
||||
BLOCK "040904e4"
|
||||
BEGIN
|
||||
VALUE "CompanyName", "com.example" "\0"
|
||||
VALUE "CompanyName", "io.appflowy" "\0"
|
||||
VALUE "FileDescription", "AppFlowy" "\0"
|
||||
VALUE "FileVersion", VERSION_AS_STRING "\0"
|
||||
VALUE "InternalName", "appflowy_flutter" "\0"
|
||||
VALUE "LegalCopyright", "Copyright (C) 2021 com.example. All rights reserved." "\0"
|
||||
VALUE "OriginalFilename", "appflowy_flutter.exe" "\0"
|
||||
VALUE "ProductName", "appflowy_flutter" "\0"
|
||||
VALUE "InternalName", "AppFlowy" "\0"
|
||||
VALUE "LegalCopyright", "Copyright (C) 2023 io.appflowy. All rights reserved." "\0"
|
||||
VALUE "OriginalFilename", "AppFlowy.exe" "\0"
|
||||
VALUE "ProductName", "AppFlowy" "\0"
|
||||
VALUE "ProductVersion", VERSION_AS_STRING "\0"
|
||||
END
|
||||
END
|
||||
|
||||
@@ -6,10 +6,12 @@
|
||||
#include "utils.h"
|
||||
|
||||
int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,
|
||||
_In_ wchar_t *command_line, _In_ int show_command) {
|
||||
_In_ wchar_t *command_line, _In_ int show_command)
|
||||
{
|
||||
// Attach to console when present (e.g., 'flutter run') or create a
|
||||
// new console when running with a debugger.
|
||||
if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) {
|
||||
if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent())
|
||||
{
|
||||
CreateAndAttachConsole();
|
||||
}
|
||||
|
||||
@@ -27,13 +29,15 @@ int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,
|
||||
FlutterWindow window(project);
|
||||
Win32Window::Point origin(10, 10);
|
||||
Win32Window::Size size(1280, 720);
|
||||
if (!window.CreateAndShow(L"appflowy_flutter", origin, size)) {
|
||||
if (!window.CreateAndShow(L"AppFlowy", origin, size))
|
||||
{
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
window.SetQuitOnClose(true);
|
||||
|
||||
::MSG msg;
|
||||
while (::GetMessage(&msg, nullptr, 0, 0)) {
|
||||
while (::GetMessage(&msg, nullptr, 0, 0))
|
||||
{
|
||||
::TranslateMessage(&msg);
|
||||
::DispatchMessage(&msg);
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
"is-hotkey": "^0.2.0",
|
||||
"jest": "^29.5.0",
|
||||
"nanoid": "^4.0.0",
|
||||
"protoc-gen-ts": "^0.8.5",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-error-boundary": "^3.1.4",
|
||||
|
||||
@@ -20,11 +20,15 @@ pub struct FlowyConfig {
|
||||
}
|
||||
|
||||
fn default_proto_output() -> String {
|
||||
"resources/proto".to_owned()
|
||||
let mut path = PathBuf::from("resources");
|
||||
path.push("proto");
|
||||
path.to_str().unwrap().to_owned()
|
||||
}
|
||||
|
||||
fn default_protobuf_crate() -> String {
|
||||
"src/protobuf".to_owned()
|
||||
let mut path = PathBuf::from("src");
|
||||
path.push("protobuf");
|
||||
path.to_str().unwrap().to_owned()
|
||||
}
|
||||
|
||||
impl FlowyConfig {
|
||||
|
||||
@@ -127,6 +127,15 @@ fn generate_ts_protobuf_files(
|
||||
}
|
||||
let protoc_bin_path = protoc_bin_path.to_str().unwrap().to_owned();
|
||||
paths.iter().for_each(|path| {
|
||||
// if let Err(err) = Command::new(protoc_bin_path.clone())
|
||||
// .arg(format!("--ts_out={}", output.to_str().unwrap()))
|
||||
// .arg(format!("--proto_path={}", proto_file_output_path))
|
||||
// .arg(path)
|
||||
// .spawn()
|
||||
// {
|
||||
// panic!("Generate ts pb file failed: {}, {:?}", path, err);
|
||||
// }
|
||||
|
||||
let result = cmd_lib::run_cmd! {
|
||||
${protoc_bin_path} --ts_out=${output} --proto_path=${proto_file_output_path} ${path}
|
||||
};
|
||||
|
||||
@@ -60,12 +60,12 @@ impl FolderDepsResolver {
|
||||
.await,
|
||||
);
|
||||
|
||||
if let (Ok(user_id), Ok(token)) = (user.user_id(), user.token()) {
|
||||
match folder_manager.initialize(&user_id, &token).await {
|
||||
Ok(_) => {},
|
||||
Err(e) => tracing::error!("Initialize folder manager failed: {}", e),
|
||||
}
|
||||
}
|
||||
// if let (Ok(user_id), Ok(token)) = (user.user_id(), user.token()) {
|
||||
// match folder_manager.initialize(&user_id, &token).await {
|
||||
// Ok(_) => {},
|
||||
// Err(e) => tracing::error!("Initialize folder manager failed: {}", e),
|
||||
// }
|
||||
// }
|
||||
|
||||
let receiver = Arc::new(FolderWSMessageReceiverImpl(folder_manager.clone()));
|
||||
ws_conn.add_ws_message_receiver(receiver).unwrap();
|
||||
@@ -339,7 +339,7 @@ impl ViewDataProcessor for DatabaseViewDataProcessor {
|
||||
}
|
||||
}
|
||||
|
||||
fn layout_type_from_view_layout(layout: ViewLayoutTypePB) -> LayoutTypePB {
|
||||
pub fn layout_type_from_view_layout(layout: ViewLayoutTypePB) -> LayoutTypePB {
|
||||
match layout {
|
||||
ViewLayoutTypePB::Grid => LayoutTypePB::Grid,
|
||||
ViewLayoutTypePB::Board => LayoutTypePB::Board,
|
||||
|
||||
@@ -25,21 +25,12 @@ impl DatabaseDepsResolver {
|
||||
) -> Arc<DatabaseManager> {
|
||||
let user = Arc::new(GridUserImpl(user_session.clone()));
|
||||
let rev_web_socket = Arc::new(GridRevisionWebSocket(ws_conn));
|
||||
let database_manager = Arc::new(DatabaseManager::new(
|
||||
user.clone(),
|
||||
Arc::new(DatabaseManager::new(
|
||||
user,
|
||||
rev_web_socket,
|
||||
task_scheduler,
|
||||
Arc::new(DatabaseDBConnectionImpl(user_session)),
|
||||
));
|
||||
|
||||
if let (Ok(user_id), Ok(token)) = (user.user_id(), user.token()) {
|
||||
match database_manager.initialize(&user_id, &token).await {
|
||||
Ok(_) => {},
|
||||
Err(e) => tracing::error!("Initialize grid manager failed: {}", e),
|
||||
}
|
||||
}
|
||||
|
||||
database_manager
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ use flowy_database::manager::DatabaseManager;
|
||||
use flowy_document::entities::DocumentVersionPB;
|
||||
use flowy_document::{DocumentConfig, DocumentManager};
|
||||
use flowy_error::FlowyResult;
|
||||
use flowy_folder::entities::ViewDataFormatPB;
|
||||
use flowy_folder::entities::{ViewDataFormatPB, ViewLayoutTypePB};
|
||||
use flowy_folder::{errors::FlowyError, manager::FolderManager};
|
||||
pub use flowy_net::get_client_server_configuration;
|
||||
use flowy_net::local_server::LocalServer;
|
||||
@@ -17,6 +17,7 @@ use flowy_user::services::{UserSession, UserSessionConfig};
|
||||
use lib_dispatch::prelude::*;
|
||||
use lib_dispatch::runtime::tokio_default_runtime;
|
||||
|
||||
use flowy_database::entities::LayoutTypePB;
|
||||
use lib_infra::future::{to_fut, Fut};
|
||||
use module::make_plugins;
|
||||
pub use module::*;
|
||||
@@ -306,7 +307,36 @@ impl UserStatusListener {
|
||||
async fn did_sign_in(&self, token: &str, user_id: &str) -> FlowyResult<()> {
|
||||
self.folder_manager.initialize(user_id, token).await?;
|
||||
self.document_manager.initialize(user_id).await?;
|
||||
self.database_manager.initialize(user_id, token).await?;
|
||||
|
||||
let cloned_folder_manager = self.folder_manager.clone();
|
||||
let get_views_fn = to_fut(async move {
|
||||
cloned_folder_manager
|
||||
.get_current_workspace()
|
||||
.await
|
||||
.map(|workspace| {
|
||||
workspace
|
||||
.apps
|
||||
.items
|
||||
.into_iter()
|
||||
.flat_map(|app| app.belongings.items)
|
||||
.flat_map(|view| match view.layout {
|
||||
ViewLayoutTypePB::Grid | ViewLayoutTypePB::Board | ViewLayoutTypePB::Calendar => {
|
||||
Some((
|
||||
view.id,
|
||||
view.name,
|
||||
layout_type_from_view_layout(view.layout),
|
||||
))
|
||||
},
|
||||
_ => None,
|
||||
})
|
||||
.collect::<Vec<(String, String, LayoutTypePB)>>()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
});
|
||||
self
|
||||
.database_manager
|
||||
.initialize(user_id, token, get_views_fn)
|
||||
.await?;
|
||||
self
|
||||
.ws_conn
|
||||
.start(token.to_owned(), user_id.to_owned())
|
||||
|
||||
@@ -7,7 +7,7 @@ use crate::services::database_view::{
|
||||
make_database_view_rev_manager, make_database_view_revision_pad, DatabaseViewEditor,
|
||||
};
|
||||
use crate::services::persistence::block_index::BlockRowIndexer;
|
||||
use crate::services::persistence::database_ref::{DatabaseInfo, DatabaseRef, DatabaseRefIndexer};
|
||||
use crate::services::persistence::database_ref::{DatabaseInfo, DatabaseRefs, DatabaseViewRef};
|
||||
use crate::services::persistence::kv::DatabaseKVPersistence;
|
||||
use crate::services::persistence::migration::DatabaseMigration;
|
||||
use crate::services::persistence::rev_sqlite::{
|
||||
@@ -29,6 +29,7 @@ use flowy_revision::{
|
||||
use flowy_sqlite::ConnectionPool;
|
||||
use flowy_task::TaskDispatcher;
|
||||
|
||||
use lib_infra::future::Fut;
|
||||
use revision_model::Revision;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::RwLock;
|
||||
@@ -43,7 +44,7 @@ pub struct DatabaseManager {
|
||||
editors_by_database_id: RwLock<HashMap<String, Arc<DatabaseEditor>>>,
|
||||
database_user: Arc<dyn DatabaseUser>,
|
||||
block_indexer: Arc<BlockRowIndexer>,
|
||||
database_ref_indexer: Arc<DatabaseRefIndexer>,
|
||||
database_refs: Arc<DatabaseRefs>,
|
||||
#[allow(dead_code)]
|
||||
kv_persistence: Arc<DatabaseKVPersistence>,
|
||||
task_scheduler: Arc<RwLock<TaskDispatcher>>,
|
||||
@@ -61,30 +62,30 @@ impl DatabaseManager {
|
||||
let editors_by_database_id = RwLock::new(HashMap::new());
|
||||
let kv_persistence = Arc::new(DatabaseKVPersistence::new(database_db.clone()));
|
||||
let block_indexer = Arc::new(BlockRowIndexer::new(database_db.clone()));
|
||||
let database_ref_indexer = Arc::new(DatabaseRefIndexer::new(database_db.clone()));
|
||||
let migration = DatabaseMigration::new(
|
||||
database_user.clone(),
|
||||
database_db,
|
||||
database_ref_indexer.clone(),
|
||||
);
|
||||
let database_refs = Arc::new(DatabaseRefs::new(database_db));
|
||||
let migration = DatabaseMigration::new(database_user.clone(), database_refs.clone());
|
||||
Self {
|
||||
editors_by_database_id,
|
||||
database_user,
|
||||
kv_persistence,
|
||||
block_indexer,
|
||||
database_ref_indexer,
|
||||
database_refs,
|
||||
task_scheduler,
|
||||
migration,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn initialize_with_new_user(&self, user_id: &str, _token: &str) -> FlowyResult<()> {
|
||||
self.migration.run(user_id).await?;
|
||||
pub async fn initialize_with_new_user(&self, _user_id: &str, _token: &str) -> FlowyResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn initialize(&self, user_id: &str, _token: &str) -> FlowyResult<()> {
|
||||
self.migration.run(user_id).await?;
|
||||
pub async fn initialize(
|
||||
&self,
|
||||
user_id: &str,
|
||||
_token: &str,
|
||||
get_views_fn: Fut<Vec<(String, String, LayoutTypePB)>>,
|
||||
) -> FlowyResult<()> {
|
||||
self.migration.run(user_id, get_views_fn).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -98,7 +99,7 @@ impl DatabaseManager {
|
||||
) -> FlowyResult<()> {
|
||||
let db_pool = self.database_user.db_pool()?;
|
||||
let _ = self
|
||||
.database_ref_indexer
|
||||
.database_refs
|
||||
.bind(database_id, view_id.as_ref(), true, name);
|
||||
let rev_manager = self.make_database_rev_manager(database_id, db_pool)?;
|
||||
rev_manager.reset_object(revisions).await?;
|
||||
@@ -113,7 +114,9 @@ impl DatabaseManager {
|
||||
revisions: Vec<Revision>,
|
||||
) -> FlowyResult<()> {
|
||||
let view_id = view_id.as_ref();
|
||||
let rev_manager = make_database_view_rev_manager(&self.database_user, view_id).await?;
|
||||
let user_id = self.database_user.user_id()?;
|
||||
let pool = self.database_user.db_pool()?;
|
||||
let rev_manager = make_database_view_rev_manager(&user_id, pool, view_id).await?;
|
||||
rev_manager.reset_object(revisions).await?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -129,12 +132,13 @@ impl DatabaseManager {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "trace", skip_all, err)]
|
||||
pub async fn open_database_view<T: AsRef<str>>(
|
||||
&self,
|
||||
view_id: T,
|
||||
) -> FlowyResult<Arc<DatabaseEditor>> {
|
||||
let view_id = view_id.as_ref();
|
||||
let database_info = self.database_ref_indexer.get_database_with_view(view_id)?;
|
||||
let database_info = self.database_refs.get_database_with_view(view_id)?;
|
||||
self
|
||||
.get_or_create_database_editor(&database_info.database_id, view_id)
|
||||
.await
|
||||
@@ -143,7 +147,7 @@ impl DatabaseManager {
|
||||
#[tracing::instrument(level = "debug", skip_all)]
|
||||
pub async fn close_database_view<T: AsRef<str>>(&self, view_id: T) -> FlowyResult<()> {
|
||||
let view_id = view_id.as_ref();
|
||||
let database_info = self.database_ref_indexer.get_database_with_view(view_id)?;
|
||||
let database_info = self.database_refs.get_database_with_view(view_id)?;
|
||||
tracing::Span::current().record("database_id", &database_info.database_id);
|
||||
|
||||
// Create a temporary reference database_editor in case of holding the write lock
|
||||
@@ -172,7 +176,7 @@ impl DatabaseManager {
|
||||
|
||||
// #[tracing::instrument(level = "debug", skip(self), err)]
|
||||
pub async fn get_database_editor(&self, view_id: &str) -> FlowyResult<Arc<DatabaseEditor>> {
|
||||
let database_info = self.database_ref_indexer.get_database_with_view(view_id)?;
|
||||
let database_info = self.database_refs.get_database_with_view(view_id)?;
|
||||
let database_editor = self
|
||||
.editors_by_database_id
|
||||
.read()
|
||||
@@ -189,13 +193,14 @@ impl DatabaseManager {
|
||||
}
|
||||
|
||||
pub async fn get_databases(&self) -> FlowyResult<Vec<DatabaseInfo>> {
|
||||
self.database_ref_indexer.get_all_databases()
|
||||
self.database_refs.get_all_databases()
|
||||
}
|
||||
|
||||
pub async fn get_database_ref_views(&self, database_id: &str) -> FlowyResult<Vec<DatabaseRef>> {
|
||||
self
|
||||
.database_ref_indexer
|
||||
.get_ref_views_with_database(database_id)
|
||||
pub async fn get_database_ref_views(
|
||||
&self,
|
||||
database_id: &str,
|
||||
) -> FlowyResult<Vec<DatabaseViewRef>> {
|
||||
self.database_refs.get_ref_views_with_database(database_id)
|
||||
}
|
||||
|
||||
async fn get_or_create_database_editor(
|
||||
@@ -277,7 +282,7 @@ impl DatabaseManager {
|
||||
database_pad,
|
||||
rev_manager,
|
||||
self.block_indexer.clone(),
|
||||
self.database_ref_indexer.clone(),
|
||||
self.database_refs.clone(),
|
||||
self.task_scheduler.clone(),
|
||||
)
|
||||
.await?;
|
||||
@@ -354,7 +359,7 @@ pub async fn link_existing_database(
|
||||
.await?;
|
||||
|
||||
let _ = database_manager
|
||||
.database_ref_indexer
|
||||
.database_refs
|
||||
.bind(database_id, view_id, false, &name);
|
||||
Ok(())
|
||||
}
|
||||
@@ -412,7 +417,6 @@ pub async fn create_new_database(
|
||||
DatabaseViewRevision::from_json(database_view_data)?
|
||||
};
|
||||
|
||||
tracing::trace!("Initial calendar layout setting: {:?}", layout_setting);
|
||||
database_view_rev.layout_settings = layout_setting;
|
||||
let database_view_ops = make_database_view_operations(&database_view_rev);
|
||||
let database_view_bytes = database_view_ops.json_bytes();
|
||||
@@ -424,14 +428,14 @@ pub async fn create_new_database(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl DatabaseRefIndexerQuery for DatabaseRefIndexer {
|
||||
fn get_ref_views(&self, database_id: &str) -> FlowyResult<Vec<DatabaseRef>> {
|
||||
impl DatabaseRefIndexerQuery for DatabaseRefs {
|
||||
fn get_ref_views(&self, database_id: &str) -> FlowyResult<Vec<DatabaseViewRef>> {
|
||||
self.get_ref_views_with_database(database_id)
|
||||
}
|
||||
}
|
||||
|
||||
impl DatabaseRefIndexerQuery for Arc<DatabaseRefIndexer> {
|
||||
fn get_ref_views(&self, database_id: &str) -> FlowyResult<Vec<DatabaseRef>> {
|
||||
impl DatabaseRefIndexerQuery for Arc<DatabaseRefs> {
|
||||
fn get_ref_views(&self, database_id: &str) -> FlowyResult<Vec<DatabaseViewRef>> {
|
||||
(**self).get_ref_views(database_id)
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,7 @@ use crate::services::database_view::{
|
||||
};
|
||||
use crate::services::filter::FilterType;
|
||||
use crate::services::persistence::block_index::BlockRowIndexer;
|
||||
use crate::services::persistence::database_ref::DatabaseRef;
|
||||
use crate::services::persistence::database_ref::DatabaseViewRef;
|
||||
use crate::services::row::{DatabaseBlockRow, DatabaseBlockRowRevision, RowRevisionBuilder};
|
||||
use bytes::Bytes;
|
||||
use database_model::*;
|
||||
@@ -42,7 +42,7 @@ use std::sync::Arc;
|
||||
use tokio::sync::{broadcast, RwLock};
|
||||
|
||||
pub trait DatabaseRefIndexerQuery: Send + Sync + 'static {
|
||||
fn get_ref_views(&self, database_id: &str) -> FlowyResult<Vec<DatabaseRef>>;
|
||||
fn get_ref_views(&self, database_id: &str) -> FlowyResult<Vec<DatabaseViewRef>>;
|
||||
}
|
||||
|
||||
pub struct DatabaseEditor {
|
||||
@@ -695,6 +695,7 @@ impl DatabaseEditor {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "trace", skip(self), err)]
|
||||
pub async fn get_database(&self, view_id: &str) -> FlowyResult<DatabasePB> {
|
||||
let pad = self.database_pad.read().await;
|
||||
let fields = pad
|
||||
@@ -887,9 +888,12 @@ impl DatabaseEditor {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn duplicate_database(&self, view_id: &str) -> FlowyResult<BuildDatabaseContext> {
|
||||
pub async fn duplicate_database(&self, _view_id: &str) -> FlowyResult<BuildDatabaseContext> {
|
||||
let database_pad = self.database_pad.read().await;
|
||||
let database_view_data = self.database_views.duplicate_database_view(view_id).await?;
|
||||
// let database_view_data = self
|
||||
// .database_views
|
||||
// .duplicate_database_view_setting(view_id)
|
||||
// .await?;
|
||||
let original_blocks = database_pad.get_block_meta_revs();
|
||||
let (duplicated_fields, duplicated_blocks) = database_pad.duplicate_database_block_meta().await;
|
||||
|
||||
@@ -918,7 +922,7 @@ impl DatabaseEditor {
|
||||
block_metas: duplicated_blocks,
|
||||
blocks: blocks_meta_data,
|
||||
layout_setting: Default::default(),
|
||||
database_view_data,
|
||||
database_view_data: "".to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -238,7 +238,7 @@ impl DatabaseViewEditor {
|
||||
self.filter_controller.filter_row_revs(rows).await;
|
||||
}
|
||||
|
||||
pub async fn v_duplicate_data(&self) -> FlowyResult<String> {
|
||||
pub async fn v_duplicate_view_setting(&self) -> FlowyResult<String> {
|
||||
let json_str = self.pad.read().await.json_str()?;
|
||||
Ok(json_str)
|
||||
}
|
||||
|
||||
@@ -101,9 +101,9 @@ impl DatabaseViews {
|
||||
Ok(row_revs)
|
||||
}
|
||||
|
||||
pub async fn duplicate_database_view(&self, view_id: &str) -> FlowyResult<String> {
|
||||
pub async fn duplicate_database_view_setting(&self, view_id: &str) -> FlowyResult<String> {
|
||||
let editor = self.get_view_editor(view_id).await?;
|
||||
let view_data = editor.v_duplicate_data().await?;
|
||||
let view_data = editor.v_duplicate_view_setting().await?;
|
||||
Ok(view_data)
|
||||
}
|
||||
|
||||
@@ -313,7 +313,9 @@ impl DatabaseViews {
|
||||
}
|
||||
|
||||
async fn make_view_editor(&self, view_id: &str) -> FlowyResult<DatabaseViewEditor> {
|
||||
let rev_manager = make_database_view_rev_manager(&self.user, view_id).await?;
|
||||
let user_id = self.user.user_id()?;
|
||||
let pool = self.user.db_pool()?;
|
||||
let rev_manager = make_database_view_rev_manager(&user_id, pool, view_id).await?;
|
||||
let user_id = self.user.user_id()?;
|
||||
let token = self.user.token()?;
|
||||
let view_id = view_id.to_owned();
|
||||
@@ -338,7 +340,9 @@ pub async fn make_database_view_revision_pad(
|
||||
DatabaseViewRevisionPad,
|
||||
RevisionManager<Arc<ConnectionPool>>,
|
||||
)> {
|
||||
let mut rev_manager = make_database_view_rev_manager(&user, view_id).await?;
|
||||
let user_id = user.user_id()?;
|
||||
let pool = user.db_pool()?;
|
||||
let mut rev_manager = make_database_view_rev_manager(&user_id, pool, view_id).await?;
|
||||
let view_rev_pad = rev_manager
|
||||
.initialize::<DatabaseViewRevisionSerde>(None)
|
||||
.await?;
|
||||
@@ -346,16 +350,14 @@ pub async fn make_database_view_revision_pad(
|
||||
}
|
||||
|
||||
pub async fn make_database_view_rev_manager(
|
||||
user: &Arc<dyn DatabaseUser>,
|
||||
user_id: &str,
|
||||
pool: Arc<ConnectionPool>,
|
||||
view_id: &str,
|
||||
) -> FlowyResult<RevisionManager<Arc<ConnectionPool>>> {
|
||||
let user_id = user.user_id()?;
|
||||
|
||||
// Create revision persistence
|
||||
let pool = user.db_pool()?;
|
||||
let disk_cache = SQLiteDatabaseViewRevisionPersistence::new(&user_id, pool.clone());
|
||||
let disk_cache = SQLiteDatabaseViewRevisionPersistence::new(user_id, pool.clone());
|
||||
let configuration = RevisionPersistenceConfiguration::new(2, false);
|
||||
let rev_persistence = RevisionPersistence::new(&user_id, view_id, disk_cache, configuration);
|
||||
let rev_persistence = RevisionPersistence::new(user_id, view_id, disk_cache, configuration);
|
||||
|
||||
// Create snapshot persistence
|
||||
const DATABASE_VIEW_SP_PREFIX: &str = "grid_view";
|
||||
@@ -365,7 +367,7 @@ pub async fn make_database_view_rev_manager(
|
||||
|
||||
let rev_compress = DatabaseViewRevisionMergeable();
|
||||
Ok(RevisionManager::new(
|
||||
&user_id,
|
||||
user_id,
|
||||
view_id,
|
||||
rev_persistence,
|
||||
rev_compress,
|
||||
|
||||
+2
-3
@@ -75,7 +75,7 @@ impl DateTypeOptionPB {
|
||||
|
||||
let time = if include_time {
|
||||
let fmt = self.time_format.format_str();
|
||||
format!("{}", naive.format_with_items(StrftimeItems::new(&fmt)))
|
||||
format!("{}", naive.format_with_items(StrftimeItems::new(fmt)))
|
||||
} else {
|
||||
"".to_string()
|
||||
};
|
||||
@@ -95,8 +95,7 @@ impl DateTypeOptionPB {
|
||||
) -> FlowyResult<i64> {
|
||||
if let Some(time_str) = time_str.as_ref() {
|
||||
if !time_str.is_empty() {
|
||||
let naive_time =
|
||||
chrono::NaiveTime::parse_from_str(&time_str, self.time_format.format_str());
|
||||
let naive_time = chrono::NaiveTime::parse_from_str(time_str, self.time_format.format_str());
|
||||
|
||||
match naive_time {
|
||||
Ok(naive_time) => {
|
||||
|
||||
@@ -7,11 +7,11 @@ use flowy_sqlite::{
|
||||
};
|
||||
use std::sync::Arc;
|
||||
|
||||
pub struct DatabaseRefIndexer {
|
||||
pub struct DatabaseRefs {
|
||||
database: Arc<dyn DatabaseDBConnection>,
|
||||
}
|
||||
|
||||
impl DatabaseRefIndexer {
|
||||
impl DatabaseRefs {
|
||||
pub fn new(database: Arc<dyn DatabaseDBConnection>) -> Self {
|
||||
Self { database }
|
||||
}
|
||||
@@ -45,7 +45,10 @@ impl DatabaseRefIndexer {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_ref_views_with_database(&self, database_id: &str) -> FlowyResult<Vec<DatabaseRef>> {
|
||||
pub fn get_ref_views_with_database(
|
||||
&self,
|
||||
database_id: &str,
|
||||
) -> FlowyResult<Vec<DatabaseViewRef>> {
|
||||
let conn = self.database.get_db_connection()?;
|
||||
let views = dsl::database_refs
|
||||
.filter(database_refs::database_id.like(database_id))
|
||||
@@ -93,12 +96,12 @@ struct DatabaseRefRecord {
|
||||
database_id: String,
|
||||
}
|
||||
|
||||
pub struct DatabaseRef {
|
||||
pub struct DatabaseViewRef {
|
||||
pub view_id: String,
|
||||
pub name: String,
|
||||
pub database_id: String,
|
||||
}
|
||||
impl std::convert::From<DatabaseRefRecord> for DatabaseRef {
|
||||
impl std::convert::From<DatabaseRefRecord> for DatabaseViewRef {
|
||||
fn from(record: DatabaseRefRecord) -> Self {
|
||||
Self {
|
||||
view_id: record.view_id,
|
||||
|
||||
+21
-15
@@ -1,6 +1,7 @@
|
||||
#![allow(clippy::all)]
|
||||
#![allow(dead_code)]
|
||||
#![allow(unused_variables)]
|
||||
use crate::services::persistence::migration::MigratedDatabase;
|
||||
use crate::services::persistence::rev_sqlite::SQLiteDatabaseRevisionPersistence;
|
||||
use bytes::Bytes;
|
||||
use database_model::DatabaseRevision;
|
||||
@@ -15,31 +16,36 @@ use lib_infra::util::md5;
|
||||
use revision_model::Revision;
|
||||
use std::sync::Arc;
|
||||
|
||||
const V1_MIGRATION: &str = "GRID_V1_MIGRATION";
|
||||
const V1_MIGRATION: &str = "DATABASE_V1_MIGRATION";
|
||||
pub fn is_database_rev_migrated(user_id: &str) -> bool {
|
||||
let key = migration_flag_key(&user_id, V1_MIGRATION);
|
||||
KV::get_bool(&key)
|
||||
}
|
||||
|
||||
pub async fn migration_database_rev_struct(
|
||||
pub(crate) async fn migration_database_rev_struct(
|
||||
user_id: &str,
|
||||
database_id: &str,
|
||||
databases: &Vec<MigratedDatabase>,
|
||||
pool: Arc<ConnectionPool>,
|
||||
) -> FlowyResult<()> {
|
||||
let key = migration_flag_key(&user_id, V1_MIGRATION, database_id);
|
||||
if KV::get_bool(&key) {
|
||||
if is_database_rev_migrated(user_id) || databases.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
let object = DatabaseRevisionResettable {
|
||||
database_id: database_id.to_owned(),
|
||||
};
|
||||
let disk_cache = SQLiteDatabaseRevisionPersistence::new(&user_id, pool);
|
||||
let reset = RevisionStructReset::new(&user_id, object, Arc::new(disk_cache));
|
||||
reset.run().await?;
|
||||
|
||||
tracing::trace!("Run database:{} v1 migration", database_id);
|
||||
tracing::debug!("Migrate databases");
|
||||
for database in databases {
|
||||
let object = DatabaseRevisionResettable {
|
||||
database_id: database.view_id.clone(),
|
||||
};
|
||||
let disk_cache = SQLiteDatabaseRevisionPersistence::new(&user_id, pool.clone());
|
||||
let reset = RevisionStructReset::new(&user_id, object, Arc::new(disk_cache));
|
||||
reset.run().await?;
|
||||
}
|
||||
let key = migration_flag_key(&user_id, V1_MIGRATION);
|
||||
KV::set_bool(&key, true);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn migration_flag_key(user_id: &str, version: &str, grid_id: &str) -> String {
|
||||
md5(format!("{}{}{}", user_id, version, grid_id,))
|
||||
fn migration_flag_key(user_id: &str, version: &str) -> String {
|
||||
md5(format!("{}{}", user_id, version,))
|
||||
}
|
||||
|
||||
struct DatabaseRevisionResettable {
|
||||
-47
@@ -1,47 +0,0 @@
|
||||
use crate::manager::DatabaseUser;
|
||||
use crate::services::database_view::make_database_view_revision_pad;
|
||||
use crate::services::persistence::database_ref::DatabaseRefIndexer;
|
||||
|
||||
use flowy_error::{FlowyError, FlowyResult};
|
||||
use flowy_sqlite::kv::KV;
|
||||
|
||||
use flowy_sqlite::{
|
||||
prelude::*,
|
||||
schema::{grid_view_rev_table, grid_view_rev_table::dsl},
|
||||
};
|
||||
use lib_infra::util::md5;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
const DATABASE_REF_INDEXING: &str = "database_ref_indexing";
|
||||
|
||||
pub async fn indexing_database_view_refs(
|
||||
user_id: &str,
|
||||
user: Arc<dyn DatabaseUser>,
|
||||
database_ref_indexer: Arc<DatabaseRefIndexer>,
|
||||
) -> FlowyResult<()> {
|
||||
let key = md5(format!("{}{}", user_id, DATABASE_REF_INDEXING));
|
||||
if KV::get_bool(&key) {
|
||||
return Ok(());
|
||||
}
|
||||
tracing::trace!("Indexing database view refs");
|
||||
let pool = user.db_pool()?;
|
||||
let view_ids = dsl::grid_view_rev_table
|
||||
.select(grid_view_rev_table::object_id)
|
||||
.distinct()
|
||||
.load::<String>(&*pool.get().map_err(|e| FlowyError::internal().context(e))?)?;
|
||||
|
||||
for view_id in view_ids {
|
||||
if let Ok((pad, _)) = make_database_view_revision_pad(&view_id, user.clone()).await {
|
||||
tracing::trace!(
|
||||
"Indexing database:{} with view:{}",
|
||||
pad.database_id,
|
||||
pad.view_id
|
||||
);
|
||||
let _ = database_ref_indexer.bind(&pad.database_id, &pad.view_id, true, &pad.name);
|
||||
}
|
||||
}
|
||||
|
||||
KV::set_bool(&key, true);
|
||||
Ok(())
|
||||
}
|
||||
+147
@@ -0,0 +1,147 @@
|
||||
use crate::services::database_view::make_database_view_rev_manager;
|
||||
use crate::services::persistence::database_ref::DatabaseRefs;
|
||||
use flowy_error::FlowyResult;
|
||||
use flowy_sqlite::kv::KV;
|
||||
|
||||
use crate::services::persistence::migration::MigratedDatabase;
|
||||
use crate::services::persistence::rev_sqlite::SQLiteDatabaseViewRevisionPersistence;
|
||||
use bytes::Bytes;
|
||||
use database_model::DatabaseViewRevision;
|
||||
use flowy_client_sync::client_database::{
|
||||
make_database_view_operations, make_database_view_rev_json_str, DatabaseViewOperationsBuilder,
|
||||
DatabaseViewRevisionPad,
|
||||
};
|
||||
use flowy_revision::reset::{RevisionResettable, RevisionStructReset};
|
||||
use flowy_sqlite::{
|
||||
prelude::*,
|
||||
schema::{grid_view_rev_table, grid_view_rev_table::dsl},
|
||||
};
|
||||
use lib_infra::util::md5;
|
||||
use revision_model::Revision;
|
||||
use std::sync::Arc;
|
||||
|
||||
const DATABASE_VIEW_MIGRATE: &str = "database_view_migrate";
|
||||
|
||||
pub fn is_database_view_migrated(user_id: &str) -> bool {
|
||||
let key = md5(format!("{}{}", user_id, DATABASE_VIEW_MIGRATE));
|
||||
KV::get_bool(&key)
|
||||
}
|
||||
|
||||
pub(crate) async fn migrate_database_view(
|
||||
user_id: &str,
|
||||
database_refs: Arc<DatabaseRefs>,
|
||||
migrated_databases: &Vec<MigratedDatabase>,
|
||||
pool: Arc<ConnectionPool>,
|
||||
) -> FlowyResult<()> {
|
||||
if is_database_view_migrated(user_id) || migrated_databases.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut database_with_view = vec![];
|
||||
|
||||
let database_without_view = {
|
||||
let conn = pool.get()?;
|
||||
let databases = migrated_databases
|
||||
.iter()
|
||||
.filter(|database| {
|
||||
let predicate = grid_view_rev_table::object_id.eq(&database.view_id);
|
||||
let exist = diesel::dsl::exists(dsl::grid_view_rev_table.filter(predicate));
|
||||
match select(exist).get_result::<bool>(&*conn) {
|
||||
Ok(is_exist) => {
|
||||
if is_exist {
|
||||
database_with_view.push((**database).clone())
|
||||
}
|
||||
!is_exist
|
||||
},
|
||||
Err(_) => true,
|
||||
}
|
||||
})
|
||||
.collect::<Vec<&MigratedDatabase>>();
|
||||
drop(conn);
|
||||
databases
|
||||
};
|
||||
|
||||
// Create database view if it's not exist.
|
||||
for database in database_without_view {
|
||||
tracing::debug!("[Migration]: create database view: {}", database.view_id);
|
||||
let database_id = database.view_id.clone();
|
||||
let database_view_id = database.view_id.clone();
|
||||
//
|
||||
let database_view_rev = DatabaseViewRevision::new(
|
||||
database_id,
|
||||
database_view_id.clone(),
|
||||
true,
|
||||
database.name.clone(),
|
||||
database.layout.clone(),
|
||||
);
|
||||
let database_view_ops = make_database_view_operations(&database_view_rev);
|
||||
let database_view_bytes = database_view_ops.json_bytes();
|
||||
let revision = Revision::initial_revision(&database_view_id, database_view_bytes);
|
||||
let rev_manager =
|
||||
make_database_view_rev_manager(user_id, pool.clone(), &database_view_id).await?;
|
||||
rev_manager.reset_object(vec![revision]).await?;
|
||||
}
|
||||
|
||||
// Reset existing database view
|
||||
for database in database_with_view {
|
||||
let object = DatabaseViewRevisionResettable {
|
||||
database_view_id: database.view_id.clone(),
|
||||
};
|
||||
let disk_cache = SQLiteDatabaseViewRevisionPersistence::new(user_id, pool.clone());
|
||||
let reset = RevisionStructReset::new(user_id, object, Arc::new(disk_cache));
|
||||
reset.run().await?;
|
||||
}
|
||||
|
||||
tracing::debug!("[Migration]: Add database view refs");
|
||||
for database in migrated_databases {
|
||||
// Bind the database with database view id. For historical reasons,
|
||||
// the default database_id is empty, so the view_id will be used
|
||||
// as the database_id.
|
||||
let database_id = database.view_id.clone();
|
||||
let database_view_id = database.view_id.clone();
|
||||
tracing::debug!(
|
||||
"Bind database:{} with view:{}",
|
||||
database_id,
|
||||
database_view_id
|
||||
);
|
||||
let _ = database_refs.bind(&database_id, &database_view_id, true, &database.name);
|
||||
}
|
||||
|
||||
let key = md5(format!("{}{}", user_id, DATABASE_VIEW_MIGRATE));
|
||||
KV::set_bool(&key, true);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
struct DatabaseViewRevisionResettable {
|
||||
database_view_id: String,
|
||||
}
|
||||
|
||||
impl RevisionResettable for DatabaseViewRevisionResettable {
|
||||
fn target_id(&self) -> &str {
|
||||
&self.database_view_id
|
||||
}
|
||||
|
||||
fn reset_data(&self, revisions: Vec<Revision>) -> FlowyResult<Bytes> {
|
||||
let pad = DatabaseViewRevisionPad::from_revisions(revisions)?;
|
||||
let json = pad.json_str()?;
|
||||
let bytes = DatabaseViewOperationsBuilder::new()
|
||||
.insert(&json)
|
||||
.build()
|
||||
.json_bytes();
|
||||
Ok(bytes)
|
||||
}
|
||||
|
||||
fn default_target_rev_str(&self) -> FlowyResult<String> {
|
||||
let database_view_rev = DatabaseViewRevision::default();
|
||||
let json = make_database_view_rev_json_str(&database_view_rev)?;
|
||||
Ok(json)
|
||||
}
|
||||
|
||||
fn read_record(&self) -> Option<String> {
|
||||
KV::get_str(self.target_id())
|
||||
}
|
||||
|
||||
fn set_record(&self, record: String) {
|
||||
KV::set_str(self.target_id(), record);
|
||||
}
|
||||
}
|
||||
@@ -1,49 +1,69 @@
|
||||
mod database_ref_indexing;
|
||||
mod database_rev_struct_migration;
|
||||
|
||||
mod database_migration;
|
||||
mod database_view_migration;
|
||||
use crate::entities::LayoutTypePB;
|
||||
use crate::manager::DatabaseUser;
|
||||
use crate::services::persistence::database_ref::DatabaseRefIndexer;
|
||||
use crate::services::persistence::migration::database_ref_indexing::indexing_database_view_refs;
|
||||
use crate::services::persistence::migration::database_rev_struct_migration::migration_database_rev_struct;
|
||||
use crate::services::persistence::DatabaseDBConnection;
|
||||
use crate::services::persistence::database_ref::DatabaseRefs;
|
||||
use crate::services::persistence::migration::database_migration::{
|
||||
is_database_rev_migrated, migration_database_rev_struct,
|
||||
};
|
||||
use crate::services::persistence::migration::database_view_migration::{
|
||||
is_database_view_migrated, migrate_database_view,
|
||||
};
|
||||
use database_model::LayoutRevision;
|
||||
use flowy_error::FlowyResult;
|
||||
use lib_infra::future::Fut;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub(crate) struct DatabaseMigration {
|
||||
#[allow(dead_code)]
|
||||
user: Arc<dyn DatabaseUser>,
|
||||
database: Arc<dyn DatabaseDBConnection>,
|
||||
database_ref_indexer: Arc<DatabaseRefIndexer>,
|
||||
database_refs: Arc<DatabaseRefs>,
|
||||
}
|
||||
|
||||
impl DatabaseMigration {
|
||||
pub fn new(
|
||||
user: Arc<dyn DatabaseUser>,
|
||||
database: Arc<dyn DatabaseDBConnection>,
|
||||
database_ref_indexer: Arc<DatabaseRefIndexer>,
|
||||
) -> Self {
|
||||
pub fn new(user: Arc<dyn DatabaseUser>, database_refs: Arc<DatabaseRefs>) -> Self {
|
||||
Self {
|
||||
user,
|
||||
database,
|
||||
database_ref_indexer,
|
||||
database_refs,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn run(&self, user_id: &str) -> FlowyResult<()> {
|
||||
let _ = indexing_database_view_refs(
|
||||
user_id,
|
||||
self.user.clone(),
|
||||
self.database_ref_indexer.clone(),
|
||||
)
|
||||
.await;
|
||||
Ok(())
|
||||
}
|
||||
pub async fn run(
|
||||
&self,
|
||||
user_id: &str,
|
||||
get_views_fn: Fut<Vec<(String, String, LayoutTypePB)>>,
|
||||
) -> FlowyResult<()> {
|
||||
let pool = self.user.db_pool()?;
|
||||
|
||||
if !is_database_view_migrated(user_id) || !is_database_rev_migrated(user_id) {
|
||||
let migrated_databases = get_views_fn
|
||||
.await
|
||||
.into_iter()
|
||||
.map(|(view_id, name, layout)| MigratedDatabase {
|
||||
view_id,
|
||||
name,
|
||||
layout: layout.into(),
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
migration_database_rev_struct(user_id, &migrated_databases, pool.clone()).await?;
|
||||
|
||||
let _ = migrate_database_view(
|
||||
user_id,
|
||||
self.database_refs.clone(),
|
||||
&migrated_databases,
|
||||
pool.clone(),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub async fn database_rev_struct_migration(&self, grid_id: &str) -> FlowyResult<()> {
|
||||
let user_id = self.user.user_id()?;
|
||||
let pool = self.database.get_db_pool()?;
|
||||
migration_database_rev_struct(&user_id, grid_id, pool).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct MigratedDatabase {
|
||||
pub(crate) view_id: String,
|
||||
pub(crate) name: String,
|
||||
pub(crate) layout: LayoutRevision,
|
||||
}
|
||||
@@ -2,7 +2,7 @@ use crate::database::block_test::util::DatabaseRowTestBuilder;
|
||||
use crate::database::database_editor::DatabaseEditorTest;
|
||||
use database_model::RowRevision;
|
||||
use flowy_database::services::database::DatabaseEditor;
|
||||
use flowy_database::services::persistence::database_ref::{DatabaseInfo, DatabaseRef};
|
||||
use flowy_database::services::persistence::database_ref::{DatabaseInfo, DatabaseViewRef};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
@@ -61,7 +61,7 @@ impl DatabaseRefTest {
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub async fn all_database_ref_views(&self, database_id: &str) -> Vec<DatabaseRef> {
|
||||
pub async fn all_database_ref_views(&self, database_id: &str) -> Vec<DatabaseViewRef> {
|
||||
self
|
||||
.inner
|
||||
.sdk
|
||||
|
||||
@@ -32,10 +32,7 @@ impl DocumentMigration {
|
||||
let conn = &*pool.get()?;
|
||||
let disk_cache = SQLiteDocumentRevisionPersistence::new(&self.user_id, pool);
|
||||
let documents = DeltaRevisionSql::read_all_documents(&self.user_id, conn)?;
|
||||
tracing::debug!(
|
||||
"[Document Migration]: try migrate {} documents",
|
||||
documents.len()
|
||||
);
|
||||
tracing::debug!("[Migration]: try migrate {} documents", documents.len());
|
||||
for revisions in documents {
|
||||
if revisions.is_empty() {
|
||||
continue;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::entities::view::ViewDataFormatPB;
|
||||
use crate::entities::{ViewLayoutTypePB, ViewPB};
|
||||
use crate::entities::{ViewLayoutTypePB, ViewPB, WorkspacePB};
|
||||
use crate::services::folder_editor::FolderRevisionMergeable;
|
||||
use crate::{
|
||||
entities::workspace::RepeatedWorkspacePB,
|
||||
@@ -21,10 +21,10 @@ use folder_model::user_default;
|
||||
use lazy_static::lazy_static;
|
||||
use lib_infra::future::FutureResult;
|
||||
|
||||
use crate::services::clear_current_workspace;
|
||||
use crate::services::persistence::rev_sqlite::{
|
||||
SQLiteFolderRevisionPersistence, SQLiteFolderRevisionSnapshotPersistence,
|
||||
};
|
||||
use crate::services::{clear_current_workspace, get_current_workspace};
|
||||
use flowy_client_sync::client_folder::FolderPad;
|
||||
use std::convert::TryFrom;
|
||||
use std::{collections::HashMap, fmt::Formatter, sync::Arc};
|
||||
@@ -209,6 +209,20 @@ impl FolderManager {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_current_workspace(&self) -> FlowyResult<WorkspacePB> {
|
||||
let user_id = self.user.user_id()?;
|
||||
let workspace_id = get_current_workspace(&user_id)?;
|
||||
let workspace = self
|
||||
.persistence
|
||||
.begin_transaction(|transaction| {
|
||||
self
|
||||
.workspace_controller
|
||||
.read_workspace(workspace_id, &user_id, &transaction)
|
||||
})
|
||||
.await?;
|
||||
Ok(workspace)
|
||||
}
|
||||
|
||||
pub async fn initialize_with_new_user(
|
||||
&self,
|
||||
user_id: &str,
|
||||
|
||||
@@ -11,6 +11,7 @@ use crate::{
|
||||
};
|
||||
use flowy_sqlite::kv::KV;
|
||||
use folder_model::{AppRevision, WorkspaceRevision};
|
||||
use lib_dispatch::prelude::ToBytes;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub struct WorkspaceController {
|
||||
@@ -41,7 +42,6 @@ impl WorkspaceController {
|
||||
) -> Result<WorkspaceRevision, FlowyError> {
|
||||
let workspace = self.create_workspace_on_server(params.clone()).await?;
|
||||
let user_id = self.user.user_id()?;
|
||||
let token = self.user.token()?;
|
||||
let workspaces = self
|
||||
.persistence
|
||||
.begin_transaction(|transaction| {
|
||||
@@ -53,9 +53,7 @@ impl WorkspaceController {
|
||||
.map(|workspace_rev| workspace_rev.into())
|
||||
.collect();
|
||||
let repeated_workspace = RepeatedWorkspacePB { items: workspaces };
|
||||
send_notification(&token, FolderNotification::DidCreateWorkspace)
|
||||
.payload(repeated_workspace)
|
||||
.send();
|
||||
send_workspace_notification(FolderNotification::DidCreateWorkspace, repeated_workspace);
|
||||
set_current_workspace(&user_id, &workspace.id);
|
||||
Ok(workspace)
|
||||
}
|
||||
@@ -76,9 +74,7 @@ impl WorkspaceController {
|
||||
})
|
||||
.await?;
|
||||
|
||||
send_notification(&workspace_id, FolderNotification::DidUpdateWorkspace)
|
||||
.payload(workspace)
|
||||
.send();
|
||||
send_workspace_notification(FolderNotification::DidUpdateWorkspace, workspace);
|
||||
self.update_workspace_on_server(params)?;
|
||||
|
||||
Ok(())
|
||||
@@ -87,7 +83,6 @@ impl WorkspaceController {
|
||||
#[allow(dead_code)]
|
||||
pub(crate) async fn delete_workspace(&self, workspace_id: &str) -> Result<(), FlowyError> {
|
||||
let user_id = self.user.user_id()?;
|
||||
let token = self.user.token()?;
|
||||
let repeated_workspace = self
|
||||
.persistence
|
||||
.begin_transaction(|transaction| {
|
||||
@@ -95,9 +90,8 @@ impl WorkspaceController {
|
||||
self.read_workspaces(None, &user_id, &transaction)
|
||||
})
|
||||
.await?;
|
||||
send_notification(&token, FolderNotification::DidDeleteWorkspace)
|
||||
.payload(repeated_workspace)
|
||||
.send();
|
||||
|
||||
send_workspace_notification(FolderNotification::DidDeleteWorkspace, repeated_workspace);
|
||||
self.delete_workspace_on_server(workspace_id)?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -224,7 +218,6 @@ pub async fn notify_workspace_setting_did_change(
|
||||
view_id: &str,
|
||||
) -> FlowyResult<()> {
|
||||
let user_id = folder_manager.user.user_id()?;
|
||||
let token = folder_manager.user.token()?;
|
||||
let workspace_id = get_current_workspace(&user_id)?;
|
||||
|
||||
let workspace_setting = folder_manager
|
||||
@@ -250,13 +243,22 @@ pub async fn notify_workspace_setting_did_change(
|
||||
Ok(setting)
|
||||
})
|
||||
.await?;
|
||||
|
||||
send_notification(&token, FolderNotification::DidUpdateWorkspaceSetting)
|
||||
.payload(workspace_setting)
|
||||
.send();
|
||||
send_workspace_notification(
|
||||
FolderNotification::DidUpdateWorkspaceSetting,
|
||||
workspace_setting,
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The [CURRENT_WORKSPACE] represents as the current workspace that opened by the
|
||||
/// user. Only one workspace can be opened at a time.
|
||||
const CURRENT_WORKSPACE: &str = "current-workspace";
|
||||
fn send_workspace_notification<T: ToBytes>(ty: FolderNotification, payload: T) {
|
||||
send_notification(CURRENT_WORKSPACE, ty)
|
||||
.payload(payload)
|
||||
.send();
|
||||
}
|
||||
|
||||
const CURRENT_WORKSPACE_ID: &str = "current_workspace_id";
|
||||
|
||||
pub fn set_current_workspace(_user_id: &str, workspace_id: &str) {
|
||||
|
||||
@@ -2,6 +2,6 @@
|
||||
Type=Application
|
||||
Name=AppFlowy
|
||||
Icon=io.appflowy.AppFlowy
|
||||
Exec=env GDK_GL=gles appflowy_flutter %U
|
||||
Exec=env GDK_GL=gles AppFlowy %U
|
||||
Categories=Network;Productivity;
|
||||
Keywords=Notes
|
||||
@@ -2,7 +2,7 @@ app-id: io.appflowy.AppFlowy
|
||||
runtime: org.freedesktop.Platform
|
||||
runtime-version: '21.08'
|
||||
sdk: org.freedesktop.Sdk
|
||||
command: appflowy_flutter
|
||||
command: AppFlowy
|
||||
separate-locales: false
|
||||
finish-args:
|
||||
- --share=ipc
|
||||
@@ -18,10 +18,10 @@ modules:
|
||||
build-commands:
|
||||
# - ls .
|
||||
- cp -r appflowy /app/appflowy
|
||||
- chmod +x /app/appflowy/appflowy_flutter
|
||||
- chmod +x /app/appflowy/AppFlowy
|
||||
- install -Dm644 logo.svg /app/share/icons/hicolor/scalable/apps/io.appflowy.AppFlowy.svg
|
||||
- mkdir /app/bin
|
||||
- ln -s /app/appflowy/appflowy_flutter /app/bin/appflowy_flutter
|
||||
- ln -s /app/appflowy/AppFlowy /app/bin/AppFlowy
|
||||
- install -Dm644 io.appflowy.AppFlowy.desktop /app/share/applications/io.appflowy.AppFlowy.desktop
|
||||
sources:
|
||||
- type: archive
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/bin/bash
|
||||
if [ -e /usr/local/bin/appflowy ]; then
|
||||
echo "Symlink already exists, skipping."
|
||||
if [ -e /usr/local/bin/AppFlowy ]; then
|
||||
echo "Symlink already exists, skipping."
|
||||
else
|
||||
echo "Creating Symlink in /usr/local/bin/appflowy"
|
||||
ln -s /opt/AppFlowy/appflowy_flutter /usr/local/bin/appflowy
|
||||
fi
|
||||
echo "Creating Symlink in /usr/local/bin/appflowy"
|
||||
ln -s /opt/AppFlowy/AppFlowy /usr/local/bin/AppFlowy
|
||||
fi
|
||||
@@ -159,7 +159,7 @@ script = [
|
||||
|
||||
[tasks.create-release-archive-macos]
|
||||
script = [
|
||||
# TODO
|
||||
# TODO
|
||||
# "cd ${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/appflowy_flutter/product/${VERSION}/${TARGET_OS}/Release/${PRODUCT_NAME}",
|
||||
# "tar -czf ${PRODUCT_NAME}-${TARGET_OS}-x86.tar.gz *"
|
||||
]
|
||||
@@ -169,7 +169,7 @@ script = ["""
|
||||
cd appflowy_flutter/
|
||||
flutter clean
|
||||
flutter pub get
|
||||
flutter build ${TARGET_OS} --${BUILD_FLAG} --build-name=${APP_VERSION}
|
||||
flutter build ${TARGET_OS} --${BUILD_FLAG}
|
||||
"""]
|
||||
script_runner = "@shell"
|
||||
|
||||
|
||||
@@ -7,15 +7,15 @@ SolidCompression=yes
|
||||
DefaultDirName={autopf}\AppFlowy\
|
||||
DefaultGroupName=AppFlowy
|
||||
SetupIconFile=flowy_logo.ico
|
||||
UninstallDisplayIcon={app}\appflowy_flutter.exe
|
||||
UninstallDisplayIcon={app}\AppFlowy.exe
|
||||
UninstallDisplayName=AppFlowy
|
||||
AppPublisher=AppFlowy-IO
|
||||
VersionInfoVersion={#AppVersion}
|
||||
|
||||
[Files]
|
||||
Source: "AppFlowy\AppFlowy.exe";DestDir: "{app}";DestName: "appflowy_flutter.exe"
|
||||
Source: "AppFlowy\AppFlowy.exe";DestDir: "{app}";DestName: "AppFlowy.exe"
|
||||
Source: "AppFlowy\*";DestDir: "{app}"
|
||||
Source: "AppFlowy\data\*";DestDir: "{app}\data\"; Flags: recursesubdirs
|
||||
|
||||
[Icons]
|
||||
Name: "{group}\AppFlowy";Filename: "{app}\appflowy_flutter.exe"
|
||||
Name: "{group}\AppFlowy";Filename: "{app}\AppFlowy.exe"
|
||||
Reference in new issue
Block a user