Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b23f6be47d |
No files matched your search
@@ -65,7 +65,7 @@ runs:
|
||||
sudo wget -qO /etc/apt/trusted.gpg.d/dart_linux_signing_key.asc https://dl-ssl.google.com/linux/linux_signing_key.pub
|
||||
sudo wget -qO /etc/apt/sources.list.d/dart_stable.list https://storage.googleapis.com/download.dartlang.org/linux/debian/dart_stable.list
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y dart curl build-essential libssl-dev clang cmake ninja-build pkg-config libgtk-3-dev keybinder-3.0 libnotify-dev libcurl4-openssl-dev
|
||||
sudo apt-get install -y dart curl build-essential libssl-dev clang cmake ninja-build pkg-config libgtk-3-dev keybinder-3.0 libnotify-dev
|
||||
;;
|
||||
Windows)
|
||||
vcpkg integrate install
|
||||
|
||||
@@ -52,7 +52,7 @@ runs:
|
||||
sudo wget -qO /etc/apt/trusted.gpg.d/dart_linux_signing_key.asc https://dl-ssl.google.com/linux/linux_signing_key.pub
|
||||
sudo wget -qO /etc/apt/sources.list.d/dart_stable.list https://storage.googleapis.com/download.dartlang.org/linux/debian/dart_stable.list
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y dart curl build-essential libssl-dev clang cmake ninja-build pkg-config libgtk-3-dev keybinder-3.0 libnotify-dev network-manager libcurl4-openssl-dev
|
||||
sudo apt-get install -y dart curl build-essential libssl-dev clang cmake ninja-build pkg-config libgtk-3-dev keybinder-3.0 libnotify-dev network-manager
|
||||
shell: bash
|
||||
|
||||
- name: Enable Flutter Desktop
|
||||
|
||||
@@ -28,7 +28,7 @@ env:
|
||||
FLUTTER_VERSION: "3.27.4"
|
||||
RUST_TOOLCHAIN: "1.81.0"
|
||||
CARGO_MAKE_VERSION: "0.37.18"
|
||||
CLOUD_VERSION: 0.9.37-amd64
|
||||
CLOUD_VERSION: 0.6.54-amd64
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
@@ -40,7 +40,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
os: [ ubuntu-latest ]
|
||||
os: [ubuntu-latest]
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
flutter_profile: development-linux-x86_64
|
||||
@@ -74,7 +74,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
os: [ windows-latest ]
|
||||
os: [windows-latest]
|
||||
include:
|
||||
- os: windows-latest
|
||||
flutter_profile: development-windows-x86
|
||||
@@ -101,7 +101,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
os: [ macos-latest ]
|
||||
os: [macos-latest]
|
||||
include:
|
||||
- os: macos-latest
|
||||
flutter_profile: development-mac-x86_64
|
||||
@@ -123,12 +123,12 @@ jobs:
|
||||
flutter_profile: ${{ matrix.flutter_profile }}
|
||||
|
||||
unit_test:
|
||||
needs: [ prepare-linux ]
|
||||
needs: [prepare-linux]
|
||||
if: github.event.pull_request.draft != true
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ ubuntu-latest ]
|
||||
os: [ubuntu-latest]
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
flutter_profile: development-linux-x86_64
|
||||
@@ -174,7 +174,7 @@ jobs:
|
||||
sudo wget -qO /etc/apt/trusted.gpg.d/dart_linux_signing_key.asc https://dl-ssl.google.com/linux/linux_signing_key.pub
|
||||
sudo wget -qO /etc/apt/sources.list.d/dart_stable.list https://storage.googleapis.com/download.dartlang.org/linux/debian/dart_stable.list
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y dart curl build-essential libssl-dev clang cmake ninja-build pkg-config libgtk-3-dev keybinder-3.0 libnotify-dev libcurl4-openssl-dev
|
||||
sudo apt-get install -y dart curl build-essential libssl-dev clang cmake ninja-build pkg-config libgtk-3-dev keybinder-3.0 libnotify-dev
|
||||
fi
|
||||
shell: bash
|
||||
|
||||
@@ -217,11 +217,11 @@ jobs:
|
||||
shell: bash
|
||||
|
||||
cloud_integration_test:
|
||||
needs: [ prepare-linux ]
|
||||
needs: [prepare-linux]
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ ubuntu-latest ]
|
||||
os: [ubuntu-latest]
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
flutter_profile: development-linux-x86_64
|
||||
@@ -308,7 +308,7 @@ jobs:
|
||||
sudo wget -qO /etc/apt/trusted.gpg.d/dart_linux_signing_key.asc https://dl-ssl.google.com/linux/linux_signing_key.pub
|
||||
sudo wget -qO /etc/apt/sources.list.d/dart_stable.list https://storage.googleapis.com/download.dartlang.org/linux/debian/dart_stable.list
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y dart curl build-essential libssl-dev clang cmake ninja-build pkg-config libgtk-3-dev keybinder-3.0 libnotify-dev libcurl4-openssl-dev
|
||||
sudo apt-get install -y dart curl build-essential libssl-dev clang cmake ninja-build pkg-config libgtk-3-dev keybinder-3.0 libnotify-dev
|
||||
shell: bash
|
||||
|
||||
- name: Enable Flutter Desktop
|
||||
@@ -340,13 +340,13 @@ jobs:
|
||||
shell: bash
|
||||
|
||||
integration_test:
|
||||
needs: [ prepare-linux ]
|
||||
needs: [prepare-linux]
|
||||
if: github.event.pull_request.draft != true
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ ubuntu-latest ]
|
||||
test_number: [ 1, 2, 3, 4, 5, 6, 7, 8, 9 ]
|
||||
os: [ubuntu-latest]
|
||||
test_number: [1, 2, 3, 4, 5, 6, 7, 8, 9]
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
target: "x86_64-unknown-linux-gnu"
|
||||
|
||||
@@ -94,26 +94,26 @@ jobs:
|
||||
cargo make --profile development-ios-arm64-sim appflowy-core-dev-ios
|
||||
cargo make --profile development-ios-arm64-sim code_generation
|
||||
|
||||
# - uses: futureware-tech/simulator-action@v3
|
||||
# id: simulator-action
|
||||
# with:
|
||||
# model: "iPhone 15"
|
||||
# shutdown_after_job: false
|
||||
- uses: futureware-tech/simulator-action@v3
|
||||
id: simulator-action
|
||||
with:
|
||||
model: "iPhone 15"
|
||||
shutdown_after_job: false
|
||||
|
||||
# - name: Run AppFlowy on simulator
|
||||
# working-directory: frontend/appflowy_flutter
|
||||
# run: |
|
||||
# flutter run -d ${{ steps.simulator-action.outputs.udid }} &
|
||||
# pid=$!
|
||||
# sleep 500
|
||||
# kill $pid
|
||||
# continue-on-error: true
|
||||
- name: Run AppFlowy on simulator
|
||||
working-directory: frontend/appflowy_flutter
|
||||
run: |
|
||||
flutter run -d ${{ steps.simulator-action.outputs.udid }} &
|
||||
pid=$!
|
||||
sleep 500
|
||||
kill $pid
|
||||
continue-on-error: true
|
||||
|
||||
# # Integration tests
|
||||
# - name: Run integration tests
|
||||
# working-directory: frontend/appflowy_flutter
|
||||
# # The integration tests are flaky and sometimes fail with "Connection timed out":
|
||||
# # Don't block the CI. If the tests fail, the CI will still pass.
|
||||
# # Instead, we're using Code Magic to re-run the tests to check if they pass.
|
||||
# continue-on-error: true
|
||||
# run: flutter test integration_test/runner.dart -d ${{ steps.simulator-action.outputs.udid }}
|
||||
# Integration tests
|
||||
- name: Run integration tests
|
||||
working-directory: frontend/appflowy_flutter
|
||||
# The integration tests are flaky and sometimes fail with "Connection timed out":
|
||||
# Don't block the CI. If the tests fail, the CI will still pass.
|
||||
# Instead, we're using Code Magic to re-run the tests to check if they pass.
|
||||
continue-on-error: true
|
||||
run: flutter test integration_test/runner.dart -d ${{ steps.simulator-action.outputs.udid }}
|
||||
@@ -366,10 +366,9 @@ jobs:
|
||||
run: |
|
||||
sudo wget -qO /etc/apt/trusted.gpg.d/dart_linux_signing_key.asc https://dl-ssl.google.com/linux/linux_signing_key.pub
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y build-essential libsqlite3-dev libssl-dev clang cmake ninja-build pkg-config libgtk-3-dev libcurl4-openssl-dev
|
||||
sudo apt-get install -y build-essential libsqlite3-dev libssl-dev clang cmake ninja-build pkg-config libgtk-3-dev
|
||||
sudo apt-get install keybinder-3.0
|
||||
sudo apt-get install -y alien libnotify-dev
|
||||
sudo apt install libfuse2
|
||||
source $HOME/.cargo/env
|
||||
cargo install --force --locked cargo-make
|
||||
cargo install --force --locked duckscript_cli
|
||||
|
||||
@@ -1,25 +1,4 @@
|
||||
# Release Notes
|
||||
## Version 0.9.0 - 30/04/2025
|
||||
### Desktop
|
||||
#### New Features
|
||||
- Added AppFlowy Prompt Library to AI Chat and Document's Ask AI
|
||||
- Revamped the desktop in-app notification center
|
||||
- Supported login with password, as well as forgot and change password options
|
||||
- Supported copying link to invite members
|
||||
- Improved the Settings' Members tab with new metadata: member avatar and joined time
|
||||
#### Bug Fixes
|
||||
- Fixed crash when trying to delete an emoji
|
||||
- Fixed Windows scaling issue
|
||||
- Correctly displayed mention text by decoding web content
|
||||
### Mobile
|
||||
#### New Features
|
||||
- Supported workspace search
|
||||
- Improved UX for links in documents
|
||||
- Supported changing password in Mobile Settings
|
||||
- Added support for inviting members via links
|
||||
#### Bug Fixes
|
||||
- Correctly displayed mention text by decoding web content
|
||||
|
||||
## Version 0.8.9 - 16/04/2025
|
||||
### Desktop
|
||||
#### New Features
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -12,38 +12,24 @@ import 'uncategorized/uncategorized_test_runner.dart'
|
||||
import 'workspace/workspace_test_runner.dart' as workspace_test_runner;
|
||||
|
||||
Future<void> main() async {
|
||||
// don't remove this test, it can prevent the test from failing.
|
||||
{
|
||||
preset_af_cloud_env_test.main();
|
||||
data_migration_test_runner.main();
|
||||
preset_af_cloud_env_test.main();
|
||||
|
||||
// uncategorized
|
||||
uncategorized_test_runner.main();
|
||||
data_migration_test_runner.main();
|
||||
|
||||
// workspace
|
||||
workspace_test_runner.main();
|
||||
}
|
||||
// uncategorized
|
||||
uncategorized_test_runner.main();
|
||||
|
||||
// sidebar
|
||||
// don't remove this test, it can prevent the test from failing.
|
||||
{
|
||||
preset_af_cloud_env_test.main();
|
||||
sidebar_move_page_test.main();
|
||||
sidebar_rename_untitled_test.main();
|
||||
sidebar_icon_test.main();
|
||||
}
|
||||
|
||||
// database
|
||||
// don't remove this test, it can prevent the test from failing.
|
||||
{
|
||||
preset_af_cloud_env_test.main();
|
||||
database_test_runner.main();
|
||||
}
|
||||
// workspace
|
||||
workspace_test_runner.main();
|
||||
|
||||
// document
|
||||
// don't remove this test, it can prevent the test from failing.
|
||||
{
|
||||
preset_af_cloud_env_test.main();
|
||||
document_test_runner.main();
|
||||
}
|
||||
document_test_runner.main();
|
||||
|
||||
// sidebar
|
||||
sidebar_move_page_test.main();
|
||||
sidebar_rename_untitled_test.main();
|
||||
sidebar_icon_test.main();
|
||||
|
||||
// database
|
||||
database_test_runner.main();
|
||||
}
|
||||
-187
@@ -1,19 +1,12 @@
|
||||
import 'package:appflowy/ai/service/ai_entities.dart';
|
||||
import 'package:appflowy/env/cloud_env.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_entities.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-ai/entities.pbenum.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import '../../../shared/ai_test_op.dart';
|
||||
import '../../../shared/constants.dart';
|
||||
import '../../../shared/mock/mock_ai.dart';
|
||||
import '../../../shared/util.dart';
|
||||
|
||||
void main() {
|
||||
@@ -24,7 +17,6 @@ void main() {
|
||||
(tester) async {
|
||||
await tester.initializeAppFlowy(
|
||||
cloudType: AuthenticatorType.appflowyCloudSelfHost,
|
||||
aiRepositoryBuilder: () => MockAIRepository(),
|
||||
);
|
||||
await tester.tapGoogleLoginInButton();
|
||||
await tester.expectToSeeHomePageWithGetStartedPage();
|
||||
@@ -51,184 +43,5 @@ void main() {
|
||||
// expect the ai writer block is not in the document
|
||||
expect(find.byType(AiWriterBlockComponent), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('Improve writing', (tester) async {
|
||||
await tester.initializeAppFlowy(
|
||||
cloudType: AuthenticatorType.appflowyCloudSelfHost,
|
||||
);
|
||||
await tester.tapGoogleLoginInButton();
|
||||
await tester.expectToSeeHomePageWithGetStartedPage();
|
||||
|
||||
const pageName = 'Document';
|
||||
await tester.createNewPageInSpace(
|
||||
spaceName: Constants.generalSpaceName,
|
||||
layout: ViewLayoutPB.Document,
|
||||
pageName: pageName,
|
||||
);
|
||||
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
|
||||
// insert a paragraph
|
||||
final text = 'I have an apple';
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
await tester.ime.insertText(text);
|
||||
await tester.editor.updateSelection(
|
||||
Selection(
|
||||
start: Position(path: [0]),
|
||||
end: Position(path: [0], offset: text.length),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tapButton(find.byType(ImproveWritingButton));
|
||||
|
||||
final editorState = tester.editor.getCurrentEditorState();
|
||||
final document = editorState.document;
|
||||
|
||||
expect(document.root.children.length, 3);
|
||||
expect(document.root.children[1].type, ParagraphBlockKeys.type);
|
||||
expect(
|
||||
document.root.children[1].delta!.toPlainText(),
|
||||
'I have an apple and a banana',
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('fix grammar', (tester) async {
|
||||
await tester.initializeAppFlowy(
|
||||
cloudType: AuthenticatorType.appflowyCloudSelfHost,
|
||||
);
|
||||
await tester.tapGoogleLoginInButton();
|
||||
await tester.expectToSeeHomePageWithGetStartedPage();
|
||||
|
||||
const pageName = 'Document';
|
||||
await tester.createNewPageInSpace(
|
||||
spaceName: Constants.generalSpaceName,
|
||||
layout: ViewLayoutPB.Document,
|
||||
pageName: pageName,
|
||||
);
|
||||
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
|
||||
// insert a paragraph
|
||||
final text = 'We didn’t had enough money';
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
await tester.ime.insertText(text);
|
||||
await tester.editor.updateSelection(
|
||||
Selection(
|
||||
start: Position(path: [0]),
|
||||
end: Position(path: [0], offset: text.length),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
await tester.selectAIWriter(AiWriterCommand.fixSpellingAndGrammar);
|
||||
|
||||
final editorState = tester.editor.getCurrentEditorState();
|
||||
final document = editorState.document;
|
||||
|
||||
expect(document.root.children.length, 3);
|
||||
expect(document.root.children[1].type, ParagraphBlockKeys.type);
|
||||
expect(
|
||||
document.root.children[1].delta!.toPlainText(),
|
||||
'We didn’t have enough money',
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('ask ai', (tester) async {
|
||||
await tester.initializeAppFlowy(
|
||||
cloudType: AuthenticatorType.appflowyCloudDevelop,
|
||||
aiRepositoryBuilder: () => MockAIRepository(
|
||||
validator: _CompletionHistoryValidator(),
|
||||
),
|
||||
);
|
||||
await tester.tapGoogleLoginInButton();
|
||||
await tester.expectToSeeHomePageWithGetStartedPage();
|
||||
|
||||
const pageName = 'Document';
|
||||
await tester.createNewPageInSpace(
|
||||
spaceName: Constants.generalSpaceName,
|
||||
layout: ViewLayoutPB.Document,
|
||||
pageName: pageName,
|
||||
);
|
||||
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
|
||||
// insert a paragraph
|
||||
final text = 'What is TPU?';
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
await tester.ime.insertText(text);
|
||||
await tester.editor.updateSelection(
|
||||
Selection(
|
||||
start: Position(path: [0]),
|
||||
end: Position(path: [0], offset: text.length),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
await tester.selectAIWriter(AiWriterCommand.userQuestion);
|
||||
|
||||
await tester.enterTextInPromptTextField("Explain the concept of TPU");
|
||||
await tester.simulateKeyEvent(LogicalKeyboardKey.enter);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// await tester.selectModel("GPT-4o-mini");
|
||||
|
||||
await tester.enterTextInPromptTextField("How about GPU?");
|
||||
await tester.simulateKeyEvent(LogicalKeyboardKey.enter);
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
class _CompletionHistoryValidator extends StreamCompletionValidator {
|
||||
static const _expectedResponses = {
|
||||
1: "What is TPU?",
|
||||
3: [
|
||||
"What is TPU?",
|
||||
"Explain the concept of TPU",
|
||||
"TPU is a tensor processing unit that is designed to accelerate machine",
|
||||
],
|
||||
5: [
|
||||
"What is TPU?",
|
||||
"Explain the concept of TPU",
|
||||
"TPU is a tensor processing unit that is designed to accelerate machine",
|
||||
"How about GPU?",
|
||||
"GPU is a graphics processing unit that is designed to accelerate machine learning tasks.",
|
||||
],
|
||||
};
|
||||
|
||||
@override
|
||||
bool validate(
|
||||
String text,
|
||||
String? objectId,
|
||||
CompletionTypePB completionType,
|
||||
PredefinedFormat? format,
|
||||
List<String> sourceIds,
|
||||
List<AiWriterRecord> history,
|
||||
) {
|
||||
assert(completionType == CompletionTypePB.UserQuestion);
|
||||
if (history.isEmpty) return false;
|
||||
|
||||
final expectedMessages = _expectedResponses[history.length];
|
||||
if (expectedMessages == null) return false;
|
||||
|
||||
if (expectedMessages is String) {
|
||||
_assertMessage(history[0].content, expectedMessages);
|
||||
return true;
|
||||
} else if (expectedMessages is List<String>) {
|
||||
for (var i = 0; i < expectedMessages.length; i++) {
|
||||
_assertMessage(history[i].content, expectedMessages[i]);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void _assertMessage(String actual, String expected) {
|
||||
assert(
|
||||
actual.trim() == expected,
|
||||
"expected '$expected', but got '${actual.trim()}'",
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -8,13 +8,11 @@ import '../../shared/util.dart';
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('Preset cloud env', () {
|
||||
testWidgets('use self-hosted cloud', (tester) async {
|
||||
group('Empty', () {
|
||||
testWidgets('set appflowy cloud', (tester) async {
|
||||
await tester.initializeAppFlowy(
|
||||
cloudType: AuthenticatorType.appflowyCloudSelfHost,
|
||||
);
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
});
|
||||
}
|
||||
+2
-2
@@ -39,8 +39,8 @@ void main() {
|
||||
);
|
||||
await tester.tapButton(find.byType(AccountSignInOutButton));
|
||||
|
||||
tester.expectToSeeText(LocaleKeys.button_yes.tr());
|
||||
await tester.tapButtonWithName(LocaleKeys.button_yes.tr());
|
||||
tester.expectToSeeText(LocaleKeys.button_ok.tr());
|
||||
await tester.tapButtonWithName(LocaleKeys.button_ok.tr());
|
||||
|
||||
// Go to the sign in page again
|
||||
await tester.pumpAndSettle(const Duration(seconds: 5));
|
||||
|
||||
@@ -80,8 +80,8 @@ void main() {
|
||||
await tester.tapGoogleLoginInButton();
|
||||
await tester.expectToSeeHomePageWithGetStartedPage();
|
||||
|
||||
expect(find.byType(AppFlowyEditorPage), findsOneWidget);
|
||||
expect(find.text('Blank page'), findsNothing);
|
||||
expect(find.byType(AppFlowyEditorPage), findsNothing);
|
||||
expect(find.text('Blank page'), findsOne);
|
||||
});
|
||||
});
|
||||
}
|
||||
+8
-8
@@ -237,11 +237,11 @@ void main() {
|
||||
await tester.tapButton(find.byType(MentionDateBlock));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// set date to be fifteenth of the last month
|
||||
// set date to be fifteenth of the next month
|
||||
await tester.tap(
|
||||
find.descendant(
|
||||
of: find.byType(DesktopAppFlowyDatePicker),
|
||||
matching: find.byFlowySvg(FlowySvgs.arrow_left_s),
|
||||
matching: find.byFlowySvg(FlowySvgs.arrow_right_s),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
@@ -275,9 +275,9 @@ void main() {
|
||||
timeFormat: UserTimeFormatPB.TwentyFourHour,
|
||||
);
|
||||
final now = DateTime.now();
|
||||
final fifteenthOfLastMonth = DateTime(now.year, now.month - 1, 15);
|
||||
final fifteenthOfNextMonth = DateTime(now.year, now.month + 1, 15);
|
||||
final formattedDate =
|
||||
dateTimeSettings.dateFormat.formatDate(fifteenthOfLastMonth, false);
|
||||
dateTimeSettings.dateFormat.formatDate(fifteenthOfNextMonth, false);
|
||||
|
||||
expect(find.byType(MentionDateBlock), findsOneWidget);
|
||||
expect(find.text('@$formattedDate'), findsOneWidget);
|
||||
@@ -378,11 +378,11 @@ void main() {
|
||||
await tester.tapButton(find.byType(MentionDateBlock));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// set date to be fifteenth of the last month
|
||||
// set date to be fifteenth of the next month
|
||||
await tester.tap(
|
||||
find.descendant(
|
||||
of: find.byType(DesktopAppFlowyDatePicker),
|
||||
matching: find.byFlowySvg(FlowySvgs.arrow_left_s),
|
||||
matching: find.byFlowySvg(FlowySvgs.arrow_right_s),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
@@ -416,9 +416,9 @@ void main() {
|
||||
timeFormat: UserTimeFormatPB.TwentyFourHour,
|
||||
);
|
||||
final now = DateTime.now();
|
||||
final fifteenthOfLastMonth = DateTime(now.year, now.month - 1, 15);
|
||||
final fifteenthOfNextMonth = DateTime(now.year, now.month + 1, 15);
|
||||
final formattedDate =
|
||||
dateTimeSettings.dateFormat.formatDate(fifteenthOfLastMonth, false);
|
||||
dateTimeSettings.dateFormat.formatDate(fifteenthOfNextMonth, false);
|
||||
|
||||
expect(find.byType(MentionDateBlock), findsOneWidget);
|
||||
expect(find.text('@$formattedDate'), findsOneWidget);
|
||||
|
||||
+19
-15
@@ -1,8 +1,13 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/startup/tasks/prelude.dart';
|
||||
import 'package:appflowy/workspace/application/settings/prelude.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import '../../shared/util.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
@@ -86,24 +91,23 @@ void main() {
|
||||
// }
|
||||
// });
|
||||
|
||||
// Disable this test because it failed after executing.
|
||||
// testWidgets('reset to default location', (tester) async {
|
||||
// await tester.initializeAppFlowy();
|
||||
testWidgets('reset to default location', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
|
||||
// await tester.tapAnonymousSignInButton();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
|
||||
// // home and readme document
|
||||
// await tester.expectToSeeHomePageWithGetStartedPage();
|
||||
// home and readme document
|
||||
await tester.expectToSeeHomePageWithGetStartedPage();
|
||||
|
||||
// // open settings and restore the location
|
||||
// await tester.openSettings();
|
||||
// await tester.openSettingsPage(SettingsPage.manageData);
|
||||
// await tester.restoreLocation();
|
||||
// open settings and restore the location
|
||||
await tester.openSettings();
|
||||
await tester.openSettingsPage(SettingsPage.manageData);
|
||||
await tester.restoreLocation();
|
||||
|
||||
// expect(
|
||||
// await appFlowyApplicationDataDirectory().then((value) => value.path),
|
||||
// await getIt<ApplicationDataStorage>().getPath(),
|
||||
// );
|
||||
// });
|
||||
expect(
|
||||
await appFlowyApplicationDataDirectory().then((value) => value.path),
|
||||
await getIt<ApplicationDataStorage>().getPath(),
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -128,12 +128,7 @@ void main() {
|
||||
/// remove the link
|
||||
await tester.tapButton(find.byFlowySvg(FlowySvgs.toolbar_link_unlink_m));
|
||||
final node = editorState.getNodeAtPath([0]);
|
||||
expect(
|
||||
node?.delta?.toJson(),
|
||||
[
|
||||
{'insert': testText},
|
||||
],
|
||||
);
|
||||
expect(node?.delta?.toJson(), [{'insert': testText}]);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -36,7 +36,7 @@ void main() {
|
||||
expect(find.byType(MobileSearchRecentList), findsNothing);
|
||||
expect(find.byType(MobileSearchResultList), findsOneWidget);
|
||||
expect(
|
||||
find.text(LocaleKeys.search_noResultForSearching.tr()),
|
||||
find.text(LocaleKeys.search_noResultForSearching.tr(args: [query])),
|
||||
findsOneWidget,
|
||||
);
|
||||
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
import 'package:appflowy/ai/ai.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/ai/ai_writer_toolbar_item.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_entities.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import 'util.dart';
|
||||
|
||||
extension AppFlowyAITest on WidgetTester {
|
||||
Future<void> selectAIWriter(AiWriterCommand command) async {
|
||||
await tapButton(find.byType(AiWriterToolbarActionList));
|
||||
await tapButton(find.text(command.i18n));
|
||||
await pumpAndSettle();
|
||||
}
|
||||
|
||||
Future<void> selectModel(String modelName) async {
|
||||
await tapButton(find.byType(SelectModelMenu));
|
||||
await tapButton(find.text(modelName));
|
||||
await pumpAndSettle();
|
||||
}
|
||||
|
||||
Future<void> enterTextInPromptTextField(String text) async {
|
||||
// Wait for the text field to be visible
|
||||
await pumpAndSettle();
|
||||
|
||||
// Find the ExtendedTextField widget
|
||||
final textField = find.descendant(
|
||||
of: find.byType(PromptInputTextField),
|
||||
matching: find.byType(TextField),
|
||||
);
|
||||
expect(textField, findsOneWidget, reason: 'ExtendedTextField not found');
|
||||
|
||||
final widget = element(textField).widget as TextField;
|
||||
expect(widget.enabled, isTrue, reason: 'TextField is not enabled');
|
||||
|
||||
await tap(textField);
|
||||
|
||||
testTextInput.enterText(text);
|
||||
await pumpAndSettle(const Duration(milliseconds: 300));
|
||||
}
|
||||
}
|
||||
@@ -28,8 +28,8 @@ extension AppFlowyAuthTest on WidgetTester {
|
||||
|
||||
await tapButton(find.byType(AccountSignInOutButton));
|
||||
|
||||
expectToSeeText(LocaleKeys.button_yes.tr());
|
||||
await tapButtonWithName(LocaleKeys.button_yes.tr());
|
||||
expectToSeeText(LocaleKeys.button_ok.tr());
|
||||
await tapButtonWithName(LocaleKeys.button_ok.tr());
|
||||
}
|
||||
|
||||
Future<void> tapSignInAsGuest() async {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:appflowy/ai/service/appflowy_ai_service.dart';
|
||||
import 'package:appflowy/env/cloud_env.dart';
|
||||
import 'package:appflowy/env/cloud_env_test.dart';
|
||||
import 'package:appflowy/startup/entry_point.dart';
|
||||
@@ -21,8 +20,6 @@ import 'package:path/path.dart' as p;
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:universal_platform/universal_platform.dart';
|
||||
|
||||
import 'mock/mock_ai.dart';
|
||||
|
||||
class FlowyTestContext {
|
||||
FlowyTestContext({required this.applicationDataDirectory});
|
||||
|
||||
@@ -36,14 +33,13 @@ extension AppFlowyTestBase on WidgetTester {
|
||||
// use to specify the application data directory, if not specified, a temporary directory will be used.
|
||||
String? dataDirectory,
|
||||
Size windowSize = const Size(1600, 1200),
|
||||
String? email,
|
||||
AuthenticatorType? cloudType,
|
||||
AIRepository Function()? aiRepositoryBuilder,
|
||||
String? email,
|
||||
}) async {
|
||||
if (Platform.isLinux || Platform.isWindows || Platform.isMacOS) {
|
||||
// Set the window size
|
||||
await binding.setSurfaceSize(windowSize);
|
||||
}
|
||||
//cloudType = AuthenticatorType.appflowyCloudDevelop;
|
||||
|
||||
mockHotKeyManagerHandlers();
|
||||
final applicationDataDirectory = dataDirectory ??
|
||||
@@ -54,12 +50,43 @@ extension AppFlowyTestBase on WidgetTester {
|
||||
await FlowyRunner.run(
|
||||
AppFlowyApplication(),
|
||||
IntegrationMode.integrationTest,
|
||||
rustEnvsBuilder: () => _buildRustEnvs(cloudType),
|
||||
didInitGetItCallback: () => _initializeCloudServices(
|
||||
cloudType: cloudType,
|
||||
email: email,
|
||||
aiRepositoryBuilder: aiRepositoryBuilder,
|
||||
),
|
||||
rustEnvsBuilder: () {
|
||||
final rustEnvs = <String, String>{};
|
||||
if (cloudType != null) {
|
||||
switch (cloudType) {
|
||||
case AuthenticatorType.local:
|
||||
break;
|
||||
case AuthenticatorType.appflowyCloudSelfHost:
|
||||
rustEnvs["GOTRUE_ADMIN_EMAIL"] = "admin@example.com";
|
||||
rustEnvs["GOTRUE_ADMIN_PASSWORD"] = "password";
|
||||
break;
|
||||
default:
|
||||
throw Exception("not supported");
|
||||
}
|
||||
}
|
||||
return rustEnvs;
|
||||
},
|
||||
didInitGetItCallback: () {
|
||||
return Future(
|
||||
() async {
|
||||
if (cloudType != null) {
|
||||
switch (cloudType) {
|
||||
case AuthenticatorType.local:
|
||||
await useLocalServer();
|
||||
break;
|
||||
case AuthenticatorType.appflowyCloudSelfHost:
|
||||
await useTestSelfHostedAppFlowyCloud();
|
||||
getIt.unregister<AuthService>();
|
||||
getIt.registerFactory<AuthService>(
|
||||
() => AppFlowyCloudMockAuthService(email: email),
|
||||
);
|
||||
default:
|
||||
throw Exception("not supported");
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
await waitUntilSignInPageShow();
|
||||
@@ -68,76 +95,6 @@ extension AppFlowyTestBase on WidgetTester {
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, String> _buildRustEnvs(AuthenticatorType? cloudType) {
|
||||
final rustEnvs = <String, String>{};
|
||||
if (cloudType != null) {
|
||||
switch (cloudType) {
|
||||
case AuthenticatorType.local:
|
||||
break;
|
||||
case AuthenticatorType.appflowyCloudSelfHost:
|
||||
case AuthenticatorType.appflowyCloudDevelop:
|
||||
rustEnvs["GOTRUE_ADMIN_EMAIL"] = "admin@example.com";
|
||||
rustEnvs["GOTRUE_ADMIN_PASSWORD"] = "password";
|
||||
break;
|
||||
default:
|
||||
throw Exception("Unsupported cloud type: $cloudType");
|
||||
}
|
||||
}
|
||||
return rustEnvs;
|
||||
}
|
||||
|
||||
Future<void> _initializeCloudServices({
|
||||
required AuthenticatorType? cloudType,
|
||||
String? email,
|
||||
AIRepository Function()? aiRepositoryBuilder,
|
||||
}) async {
|
||||
if (cloudType == null) return;
|
||||
|
||||
switch (cloudType) {
|
||||
case AuthenticatorType.local:
|
||||
await useLocalServer();
|
||||
break;
|
||||
case AuthenticatorType.appflowyCloudSelfHost:
|
||||
await _setupAppFlowyCloud(
|
||||
useLocal: false,
|
||||
email: email,
|
||||
aiRepositoryBuilder: aiRepositoryBuilder,
|
||||
);
|
||||
break;
|
||||
case AuthenticatorType.appflowyCloudDevelop:
|
||||
await _setupAppFlowyCloud(
|
||||
useLocal: integrationMode().isDevelop,
|
||||
email: email,
|
||||
aiRepositoryBuilder: aiRepositoryBuilder,
|
||||
);
|
||||
break;
|
||||
default:
|
||||
throw Exception("Unsupported cloud type: $cloudType");
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _setupAppFlowyCloud({
|
||||
required bool useLocal,
|
||||
String? email,
|
||||
AIRepository Function()? aiRepositoryBuilder,
|
||||
}) async {
|
||||
if (useLocal) {
|
||||
await useAppFlowyCloudDevelop("http://localhost");
|
||||
} else {
|
||||
await useSelfHostedAppFlowyCloud(TestEnv.afCloudUrl);
|
||||
}
|
||||
|
||||
getIt.unregister<AuthService>();
|
||||
getIt.unregister<AIRepository>();
|
||||
|
||||
getIt.registerFactory<AuthService>(
|
||||
() => AppFlowyCloudMockAuthService(email: email),
|
||||
);
|
||||
getIt.registerFactory<AIRepository>(
|
||||
aiRepositoryBuilder ?? () => MockAIRepository(),
|
||||
);
|
||||
}
|
||||
|
||||
void mockHotKeyManagerHandlers() {
|
||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||
.setMockMethodCallHandler(const MethodChannel('hotkey_manager'),
|
||||
@@ -318,6 +275,10 @@ extension AppFlowyFinderTestBase on CommonFinders {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> useTestSelfHostedAppFlowyCloud() async {
|
||||
await useSelfHostedAppFlowyCloudWithURL(TestEnv.afCloudUrl);
|
||||
}
|
||||
|
||||
Future<String> mockApplicationDataStorage({
|
||||
// use to append after the application data directory
|
||||
String? pathExtension,
|
||||
|
||||
@@ -1,128 +0,0 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:appflowy/ai/service/ai_entities.dart';
|
||||
import 'package:appflowy/ai/service/appflowy_ai_service.dart';
|
||||
import 'package:appflowy/ai/service/error.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_entities.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-ai/entities.pbenum.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
|
||||
final _mockAiMap = <CompletionTypePB, Map<String, List<String>>>{
|
||||
CompletionTypePB.ImproveWriting: {
|
||||
"I have an apple": [
|
||||
"I",
|
||||
"have",
|
||||
"an",
|
||||
"apple",
|
||||
"and",
|
||||
"a",
|
||||
"banana",
|
||||
],
|
||||
},
|
||||
CompletionTypePB.SpellingAndGrammar: {
|
||||
"We didn’t had enough money": [
|
||||
"We",
|
||||
"didn’t",
|
||||
"have",
|
||||
"enough",
|
||||
"money",
|
||||
],
|
||||
},
|
||||
CompletionTypePB.UserQuestion: {
|
||||
"Explain the concept of TPU": [
|
||||
"TPU",
|
||||
"is",
|
||||
"a",
|
||||
"tensor",
|
||||
"processing",
|
||||
"unit",
|
||||
"that",
|
||||
"is",
|
||||
"designed",
|
||||
"to",
|
||||
"accelerate",
|
||||
"machine",
|
||||
],
|
||||
"How about GPU?": [
|
||||
"GPU",
|
||||
"is",
|
||||
"a",
|
||||
"graphics",
|
||||
"processing",
|
||||
"unit",
|
||||
"that",
|
||||
"is",
|
||||
"designed",
|
||||
"to",
|
||||
"accelerate",
|
||||
"machine",
|
||||
"learning",
|
||||
"tasks",
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
abstract class StreamCompletionValidator {
|
||||
bool validate(
|
||||
String text,
|
||||
String? objectId,
|
||||
CompletionTypePB completionType,
|
||||
PredefinedFormat? format,
|
||||
List<String> sourceIds,
|
||||
List<AiWriterRecord> history,
|
||||
);
|
||||
}
|
||||
|
||||
class MockCompletionStream extends Mock implements CompletionStream {}
|
||||
|
||||
class MockAIRepository extends Mock implements AppFlowyAIService {
|
||||
MockAIRepository({this.validator});
|
||||
StreamCompletionValidator? validator;
|
||||
|
||||
@override
|
||||
Future<(String, CompletionStream)?> streamCompletion({
|
||||
String? objectId,
|
||||
required String text,
|
||||
PredefinedFormat? format,
|
||||
List<String> sourceIds = const [],
|
||||
List<AiWriterRecord> history = const [],
|
||||
required CompletionTypePB completionType,
|
||||
required Future<void> Function() onStart,
|
||||
required Future<void> Function(String text) processMessage,
|
||||
required Future<void> Function(String text) processAssistMessage,
|
||||
required Future<void> Function() onEnd,
|
||||
required void Function(AIError error) onError,
|
||||
required void Function(LocalAIStreamingState state)
|
||||
onLocalAIStreamingStateChange,
|
||||
}) async {
|
||||
if (validator != null) {
|
||||
if (!validator!.validate(
|
||||
text,
|
||||
objectId,
|
||||
completionType,
|
||||
format,
|
||||
sourceIds,
|
||||
history,
|
||||
)) {
|
||||
throw Exception('Invalid completion');
|
||||
}
|
||||
}
|
||||
final stream = MockCompletionStream();
|
||||
unawaited(
|
||||
Future(() async {
|
||||
await onStart();
|
||||
final lines = _mockAiMap[completionType]?[text.trim()];
|
||||
|
||||
if (lines == null) {
|
||||
throw Exception('No mock ai found for $text and $completionType');
|
||||
}
|
||||
|
||||
for (final line in lines) {
|
||||
await processMessage('$line ');
|
||||
}
|
||||
await onEnd();
|
||||
}),
|
||||
);
|
||||
return ('mock_id', stream);
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import 'package:appflowy/workspace/presentation/settings/settings_dialog.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/settings_menu_element.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text_field.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
@@ -85,7 +86,7 @@ extension AppFlowySettings on WidgetTester {
|
||||
|
||||
final userNameFinder = find.descendant(
|
||||
of: find.byType(AccountUserProfile),
|
||||
matching: find.byType(TextField),
|
||||
matching: find.byType(FlowyTextField),
|
||||
);
|
||||
await enterText(userNameFinder, name);
|
||||
await pumpAndSettle();
|
||||
|
||||
@@ -8,35 +8,35 @@ PODS:
|
||||
- ReachabilitySwift
|
||||
- device_info_plus (0.0.1):
|
||||
- Flutter
|
||||
- DKImagePickerController/Core (4.3.9):
|
||||
- DKImagePickerController/Core (4.3.4):
|
||||
- DKImagePickerController/ImageDataManager
|
||||
- DKImagePickerController/Resource
|
||||
- DKImagePickerController/ImageDataManager (4.3.9)
|
||||
- DKImagePickerController/PhotoGallery (4.3.9):
|
||||
- DKImagePickerController/ImageDataManager (4.3.4)
|
||||
- DKImagePickerController/PhotoGallery (4.3.4):
|
||||
- DKImagePickerController/Core
|
||||
- DKPhotoGallery
|
||||
- DKImagePickerController/Resource (4.3.9)
|
||||
- DKPhotoGallery (0.0.19):
|
||||
- DKPhotoGallery/Core (= 0.0.19)
|
||||
- DKPhotoGallery/Model (= 0.0.19)
|
||||
- DKPhotoGallery/Preview (= 0.0.19)
|
||||
- DKPhotoGallery/Resource (= 0.0.19)
|
||||
- DKImagePickerController/Resource (4.3.4)
|
||||
- DKPhotoGallery (0.0.17):
|
||||
- DKPhotoGallery/Core (= 0.0.17)
|
||||
- DKPhotoGallery/Model (= 0.0.17)
|
||||
- DKPhotoGallery/Preview (= 0.0.17)
|
||||
- DKPhotoGallery/Resource (= 0.0.17)
|
||||
- SDWebImage
|
||||
- SwiftyGif
|
||||
- DKPhotoGallery/Core (0.0.19):
|
||||
- DKPhotoGallery/Core (0.0.17):
|
||||
- DKPhotoGallery/Model
|
||||
- DKPhotoGallery/Preview
|
||||
- SDWebImage
|
||||
- SwiftyGif
|
||||
- DKPhotoGallery/Model (0.0.19):
|
||||
- DKPhotoGallery/Model (0.0.17):
|
||||
- SDWebImage
|
||||
- SwiftyGif
|
||||
- DKPhotoGallery/Preview (0.0.19):
|
||||
- DKPhotoGallery/Preview (0.0.17):
|
||||
- DKPhotoGallery/Model
|
||||
- DKPhotoGallery/Resource
|
||||
- SDWebImage
|
||||
- SwiftyGif
|
||||
- DKPhotoGallery/Resource (0.0.19):
|
||||
- DKPhotoGallery/Resource (0.0.17):
|
||||
- SDWebImage
|
||||
- SwiftyGif
|
||||
- file_picker (0.0.1):
|
||||
@@ -65,17 +65,17 @@ PODS:
|
||||
- FlutterMacOS
|
||||
- permission_handler_apple (9.3.0):
|
||||
- Flutter
|
||||
- ReachabilitySwift (5.2.4)
|
||||
- ReachabilitySwift (5.0.0)
|
||||
- saver_gallery (0.0.1):
|
||||
- Flutter
|
||||
- SDWebImage (5.21.0):
|
||||
- SDWebImage/Core (= 5.21.0)
|
||||
- SDWebImage/Core (5.21.0)
|
||||
- Sentry/HybridSDK (8.46.0)
|
||||
- sentry_flutter (8.14.2):
|
||||
- SDWebImage (5.14.2):
|
||||
- SDWebImage/Core (= 5.14.2)
|
||||
- SDWebImage/Core (5.14.2)
|
||||
- Sentry/HybridSDK (8.35.1)
|
||||
- sentry_flutter (8.8.0):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- Sentry/HybridSDK (= 8.46.0)
|
||||
- Sentry/HybridSDK (= 8.35.1)
|
||||
- share_plus (0.0.1):
|
||||
- Flutter
|
||||
- shared_preferences_foundation (0.0.1):
|
||||
@@ -86,8 +86,8 @@ PODS:
|
||||
- FlutterMacOS
|
||||
- super_native_extensions (0.0.1):
|
||||
- Flutter
|
||||
- SwiftyGif (5.4.5)
|
||||
- Toast (4.1.1)
|
||||
- SwiftyGif (5.4.3)
|
||||
- Toast (4.0.0)
|
||||
- url_launcher_ios (0.0.1):
|
||||
- Flutter
|
||||
- webview_flutter_wkwebview (0.0.1):
|
||||
@@ -185,8 +185,8 @@ SPEC CHECKSUMS:
|
||||
appflowy_backend: 78f6a053f756e6bc29bcc5a2106cbe77b756e97a
|
||||
connectivity_plus: 481668c94744c30c53b8895afb39159d1e619bdf
|
||||
device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe
|
||||
DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
|
||||
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
|
||||
DKImagePickerController: b512c28220a2b8ac7419f21c491fc8534b7601ac
|
||||
DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179
|
||||
file_picker: 9b3292d7c8bc68c8a7bf8eb78f730e49c8efc517
|
||||
flowy_infra_ui: 931b73a18b54a392ab6152eebe29a63a30751f53
|
||||
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
|
||||
@@ -199,17 +199,17 @@ SPEC CHECKSUMS:
|
||||
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
|
||||
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
|
||||
permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d
|
||||
ReachabilitySwift: 32793e867593cfc1177f5d16491e3a197d2fccda
|
||||
ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825
|
||||
saver_gallery: af2d0c762dafda254e0ad025ef0dabd6506cd490
|
||||
SDWebImage: f84b0feeb08d2d11e6a9b843cb06d75ebf5b8868
|
||||
Sentry: da60d980b197a46db0b35ea12cb8f39af48d8854
|
||||
sentry_flutter: 27892878729f42701297c628eb90e7c6529f3684
|
||||
SDWebImage: b9a731e1d6307f44ca703b3976d18c24ca561e84
|
||||
Sentry: 1fe34e9c2cbba1e347623610d26db121dcb569f1
|
||||
sentry_flutter: e24b397f9a61fa5bbefd8279c3b2242ca86faa90
|
||||
share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a
|
||||
shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7
|
||||
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
|
||||
super_native_extensions: b763c02dc3a8fd078389f410bf15149179020cb4
|
||||
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
|
||||
Toast: 1f5ea13423a1e6674c4abdac5be53587ae481c4e
|
||||
SwiftyGif: 6c3eafd0ce693cad58bb63d2b2fb9bacb8552780
|
||||
Toast: 91b396c56ee72a5790816f40d3a94dd357abc196
|
||||
url_launcher_ios: 694010445543906933d732453a59da0a173ae33d
|
||||
webview_flutter_wkwebview: 44d4dee7d7056d5ad185d25b38404436d56c547c
|
||||
|
||||
|
||||
@@ -122,6 +122,8 @@ enum AiPromptCategory {
|
||||
business,
|
||||
@JsonValue("marketing")
|
||||
marketing,
|
||||
@JsonValue("learning")
|
||||
learning,
|
||||
@JsonValue("travel")
|
||||
travel,
|
||||
@JsonValue("contentSeo")
|
||||
@@ -143,17 +145,7 @@ enum AiPromptCategory {
|
||||
@JsonValue("caseStudies")
|
||||
caseStudies,
|
||||
@JsonValue("salesCopy")
|
||||
salesCopy,
|
||||
@JsonValue("education")
|
||||
education,
|
||||
@JsonValue("work")
|
||||
work,
|
||||
@JsonValue("podcastProduction")
|
||||
podcastProduction,
|
||||
@JsonValue("copyWriting")
|
||||
copyWriting,
|
||||
@JsonValue("customerSuccess")
|
||||
customerSuccess;
|
||||
salesCopy;
|
||||
|
||||
String get i18n {
|
||||
return switch (this) {
|
||||
@@ -163,6 +155,7 @@ enum AiPromptCategory {
|
||||
healthAndFitness => LocaleKeys.ai_customPrompt_healthAndFitness.tr(),
|
||||
business => LocaleKeys.ai_customPrompt_business.tr(),
|
||||
marketing => LocaleKeys.ai_customPrompt_marketing.tr(),
|
||||
learning => LocaleKeys.ai_customPrompt_learning.tr(),
|
||||
travel => LocaleKeys.ai_customPrompt_travel.tr(),
|
||||
contentSeo => LocaleKeys.ai_customPrompt_contentSeo.tr(),
|
||||
emailMarketing => LocaleKeys.ai_customPrompt_emailMarketing.tr(),
|
||||
@@ -174,11 +167,6 @@ enum AiPromptCategory {
|
||||
strategy => LocaleKeys.ai_customPrompt_strategy.tr(),
|
||||
caseStudies => LocaleKeys.ai_customPrompt_caseStudies.tr(),
|
||||
salesCopy => LocaleKeys.ai_customPrompt_salesCopy.tr(),
|
||||
education => LocaleKeys.ai_customPrompt_education.tr(),
|
||||
work => LocaleKeys.ai_customPrompt_work.tr(),
|
||||
podcastProduction => LocaleKeys.ai_customPrompt_podcastProduction.tr(),
|
||||
copyWriting => LocaleKeys.ai_customPrompt_copyWriting.tr(),
|
||||
customerSuccess => LocaleKeys.ai_customPrompt_customerSuccess.tr(),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -191,6 +179,7 @@ class AiPrompt extends Equatable {
|
||||
required this.content,
|
||||
required this.category,
|
||||
required this.example,
|
||||
required this.sampleResponse,
|
||||
required this.isFeatured,
|
||||
});
|
||||
|
||||
@@ -202,16 +191,14 @@ class AiPrompt extends Equatable {
|
||||
final String id;
|
||||
final String name;
|
||||
final String content;
|
||||
@JsonKey(
|
||||
unknownEnumValue: AiPromptCategory.other,
|
||||
defaultValue: AiPromptCategory.other,
|
||||
)
|
||||
final AiPromptCategory category;
|
||||
@JsonKey(defaultValue: "")
|
||||
final String example;
|
||||
@JsonKey(defaultValue: "")
|
||||
final String sampleResponse;
|
||||
@JsonKey(defaultValue: false)
|
||||
final bool isFeatured;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [id, name, content, category, example, isFeatured];
|
||||
List<Object?> get props => [id, name, content, category, sampleResponse];
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-ai/entities.pb.dart';
|
||||
import 'package:appflowy_result/appflowy_result.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:protobuf/protobuf.dart';
|
||||
import 'package:universal_platform/universal_platform.dart';
|
||||
|
||||
typedef OnModelStateChangedCallback = void Function(AIModelState state);
|
||||
@@ -51,29 +52,25 @@ class AIModelStateNotifier {
|
||||
final String objectId;
|
||||
final LocalAIStateListener? _localAIListener;
|
||||
final AIModelSwitchListener _aiModelSwitchListener;
|
||||
|
||||
LocalAIPB? _localAIState;
|
||||
ModelSelectionPB? _modelSelection;
|
||||
|
||||
AIModelState _currentState = _defaultState();
|
||||
List<AIModelPB> _availableModels = [];
|
||||
AIModelPB? _selectedModel;
|
||||
AvailableModelsPB? _availableModels;
|
||||
|
||||
// callbacks
|
||||
final List<OnModelStateChangedCallback> _stateChangedCallbacks = [];
|
||||
final List<OnAvailableModelsChangedCallback>
|
||||
_availableModelsChangedCallbacks = [];
|
||||
|
||||
/// Starts platform-specific listeners
|
||||
void _startListening() {
|
||||
if (UniversalPlatform.isDesktop) {
|
||||
_localAIListener?.start(
|
||||
stateCallback: (state) async {
|
||||
_localAIState = state;
|
||||
_updateAll();
|
||||
_notifyStateChanged();
|
||||
|
||||
if (state.state == RunningStatePB.Running ||
|
||||
state.state == RunningStatePB.Stopped) {
|
||||
await _loadModelSelection();
|
||||
_updateAll();
|
||||
await _loadAvailableModels();
|
||||
_notifyAvailableModelsChanged();
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -81,25 +78,25 @@ class AIModelStateNotifier {
|
||||
|
||||
_aiModelSwitchListener.start(
|
||||
onUpdateSelectedModel: (model) async {
|
||||
_selectedModel = model;
|
||||
_updateAll();
|
||||
final updatedModels = _availableModels?.deepCopy()
|
||||
?..selectedModel = model;
|
||||
_availableModels = updatedModels;
|
||||
_notifyAvailableModelsChanged();
|
||||
|
||||
if (model.isLocal && UniversalPlatform.isDesktop) {
|
||||
await _loadLocalState();
|
||||
_updateAll();
|
||||
await _loadLocalAiState();
|
||||
}
|
||||
_notifyStateChanged();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _init() async {
|
||||
await Future.wait([
|
||||
if (UniversalPlatform.isDesktop) _loadLocalState(),
|
||||
_loadModelSelection(),
|
||||
]);
|
||||
_updateAll();
|
||||
void _init() async {
|
||||
await Future.wait([_loadLocalAiState(), _loadAvailableModels()]);
|
||||
_notifyStateChanged();
|
||||
_notifyAvailableModelsChanged();
|
||||
}
|
||||
|
||||
/// Register callbacks for state or available-models changes
|
||||
void addListener({
|
||||
OnModelStateChangedCallback? onStateChanged,
|
||||
OnAvailableModelsChangedCallback? onAvailableModelsChanged,
|
||||
@@ -112,7 +109,6 @@ class AIModelStateNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove previously registered callbacks
|
||||
void removeListener({
|
||||
OnModelStateChangedCallback? onStateChanged,
|
||||
OnAvailableModelsChangedCallback? onAvailableModelsChanged,
|
||||
@@ -132,88 +128,116 @@ class AIModelStateNotifier {
|
||||
await _aiModelSwitchListener.stop();
|
||||
}
|
||||
|
||||
/// Returns current AIModelState
|
||||
AIModelState getState() => _currentState;
|
||||
|
||||
/// Returns available models and the selected model
|
||||
(List<AIModelPB>, AIModelPB?) getModelSelection() =>
|
||||
(_availableModels, _selectedModel);
|
||||
|
||||
void _updateAll() {
|
||||
_currentState = _computeState();
|
||||
for (final cb in _stateChangedCallbacks) {
|
||||
cb(_currentState);
|
||||
}
|
||||
for (final cb in _availableModelsChangedCallbacks) {
|
||||
cb(_availableModels, _selectedModel);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _loadModelSelection() async {
|
||||
await AIEventGetSourceModelSelection(
|
||||
ModelSourcePB(source: objectId),
|
||||
).send().fold(
|
||||
(ms) {
|
||||
_modelSelection = ms;
|
||||
_availableModels = ms.models;
|
||||
_selectedModel = ms.selectedModel;
|
||||
},
|
||||
(e) => Log.error("Failed to fetch models: \$e"),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _loadLocalState() async {
|
||||
await AIEventGetLocalAIState().send().fold(
|
||||
(s) => _localAIState = s,
|
||||
(e) => Log.error("Failed to fetch local AI state: \$e"),
|
||||
);
|
||||
}
|
||||
|
||||
static AIModelState _defaultState() => AIModelState(
|
||||
AIModelState getState() {
|
||||
if (UniversalPlatform.isMobile) {
|
||||
return AIModelState(
|
||||
type: AiType.cloud,
|
||||
hintText: LocaleKeys.chat_inputMessageHint.tr(),
|
||||
tooltip: null,
|
||||
isEditable: true,
|
||||
localAIEnabled: false,
|
||||
);
|
||||
|
||||
/// Core logic computing the state from local and selection data
|
||||
AIModelState _computeState() {
|
||||
if (UniversalPlatform.isMobile) return _defaultState();
|
||||
|
||||
if (_modelSelection == null || _localAIState == null) {
|
||||
return _defaultState();
|
||||
}
|
||||
|
||||
if (!_selectedModel!.isLocal) {
|
||||
return _defaultState();
|
||||
final availableModels = _availableModels;
|
||||
final localAiState = _localAIState;
|
||||
|
||||
if (availableModels == null) {
|
||||
return AIModelState(
|
||||
type: AiType.cloud,
|
||||
hintText: LocaleKeys.chat_inputMessageHint.tr(),
|
||||
isEditable: true,
|
||||
tooltip: null,
|
||||
localAIEnabled: false,
|
||||
);
|
||||
}
|
||||
if (localAiState == null) {
|
||||
return AIModelState(
|
||||
type: AiType.cloud,
|
||||
hintText: LocaleKeys.chat_inputMessageHint.tr(),
|
||||
tooltip: null,
|
||||
isEditable: true,
|
||||
localAIEnabled: false,
|
||||
);
|
||||
}
|
||||
|
||||
final enabled = _localAIState!.enabled;
|
||||
final running = _localAIState!.state == RunningStatePB.Running;
|
||||
final hintKey = enabled
|
||||
? (running
|
||||
? LocaleKeys.chat_inputLocalAIMessageHint
|
||||
: LocaleKeys.settings_aiPage_keys_localAIInitializing)
|
||||
: LocaleKeys.settings_aiPage_keys_localAIDisabled;
|
||||
final tooltipKey = enabled
|
||||
? (running
|
||||
if (!availableModels.selectedModel.isLocal) {
|
||||
return AIModelState(
|
||||
type: AiType.cloud,
|
||||
hintText: LocaleKeys.chat_inputMessageHint.tr(),
|
||||
tooltip: null,
|
||||
isEditable: true,
|
||||
localAIEnabled: false,
|
||||
);
|
||||
}
|
||||
|
||||
final editable = localAiState.state == RunningStatePB.Running;
|
||||
final tooltip = localAiState.enabled
|
||||
? (editable
|
||||
? null
|
||||
: LocaleKeys.settings_aiPage_keys_localAINotReadyTextFieldPrompt)
|
||||
: LocaleKeys.settings_aiPage_keys_localAIDisabledTextFieldPrompt;
|
||||
: LocaleKeys.settings_aiPage_keys_localAINotReadyTextFieldPrompt
|
||||
.tr())
|
||||
: LocaleKeys.settings_aiPage_keys_localAIDisabledTextFieldPrompt.tr();
|
||||
|
||||
final hintText = localAiState.enabled
|
||||
? (editable
|
||||
? LocaleKeys.chat_inputLocalAIMessageHint.tr()
|
||||
: LocaleKeys.settings_aiPage_keys_localAIInitializing.tr())
|
||||
: LocaleKeys.settings_aiPage_keys_localAIDisabled.tr();
|
||||
|
||||
return AIModelState(
|
||||
type: AiType.local,
|
||||
hintText: hintKey.tr(),
|
||||
tooltip: tooltipKey?.tr(),
|
||||
isEditable: running,
|
||||
localAIEnabled: enabled,
|
||||
hintText: hintText,
|
||||
tooltip: tooltip,
|
||||
isEditable: editable,
|
||||
localAIEnabled: localAiState.enabled,
|
||||
);
|
||||
}
|
||||
|
||||
(List<AIModelPB>, AIModelPB?) getAvailableModels() {
|
||||
final availableModels = _availableModels;
|
||||
if (availableModels == null) {
|
||||
return ([], null);
|
||||
}
|
||||
return (availableModels.models, availableModels.selectedModel);
|
||||
}
|
||||
|
||||
void _notifyAvailableModelsChanged() {
|
||||
final (models, selectedModel) = getAvailableModels();
|
||||
for (final callback in _availableModelsChangedCallbacks) {
|
||||
callback(models, selectedModel);
|
||||
}
|
||||
}
|
||||
|
||||
void _notifyStateChanged() {
|
||||
final state = getState();
|
||||
for (final callback in _stateChangedCallbacks) {
|
||||
callback(state);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _loadAvailableModels() {
|
||||
final payload = AvailableModelsQueryPB(source: objectId);
|
||||
return AIEventGetAvailableModels(payload).send().fold(
|
||||
(models) => _availableModels = models,
|
||||
(err) => Log.error("Failed to get available models: $err"),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _loadLocalAiState() {
|
||||
return AIEventGetLocalAIState().send().fold(
|
||||
(localAIState) => _localAIState = localAIState,
|
||||
(error) => Log.error("Failed to get local AI state: $error"),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
extension AIModelPBExtension on AIModelPB {
|
||||
bool get isDefault => name == 'Auto';
|
||||
String get i18n =>
|
||||
isDefault ? LocaleKeys.chat_switchModel_autoModel.tr() : name;
|
||||
extension AiModelExtension on AIModelPB {
|
||||
bool get isDefault {
|
||||
return name == "Auto";
|
||||
}
|
||||
|
||||
String get i18n {
|
||||
return isDefault ? LocaleKeys.chat_switchModel_autoModel.tr() : name;
|
||||
}
|
||||
}
|
||||
@@ -29,14 +29,12 @@ class AiPromptSelectorCubit extends Cubit<AiPromptSelectorState> {
|
||||
|
||||
void _loadPrompts() async {
|
||||
availablePrompts.addAll(await _aiService.getBuiltInPrompts());
|
||||
final featuredPrompts =
|
||||
availablePrompts.where((prompt) => prompt.isFeatured);
|
||||
final visiblePrompts = _getFilteredPrompts(featuredPrompts);
|
||||
final visiblePrompts = _getFilteredPrompts(availablePrompts);
|
||||
emit(
|
||||
AiPromptSelectorState.ready(
|
||||
visiblePrompts: visiblePrompts,
|
||||
showCustomPrompts: false,
|
||||
isFeaturedCategorySelected: true,
|
||||
isFeaturedCategorySelected: false,
|
||||
selectedPromptId: visiblePrompts.firstOrNull?.id,
|
||||
selectedCategory: null,
|
||||
favoritePrompts: [],
|
||||
@@ -47,13 +45,12 @@ class AiPromptSelectorCubit extends Cubit<AiPromptSelectorState> {
|
||||
void selectFeaturedCategory() {
|
||||
state.maybeMap(
|
||||
ready: (readyState) {
|
||||
final prompts = availablePrompts.where((prompt) => prompt.isFeatured);
|
||||
final visiblePrompts = _getFilteredPrompts(prompts);
|
||||
emit(
|
||||
readyState.copyWith(
|
||||
visiblePrompts: visiblePrompts,
|
||||
// TODO(RS): Add logic to filter prompts based on the featured category
|
||||
// visiblePrompts: prompts,
|
||||
isFeaturedCategorySelected: true,
|
||||
selectedPromptId: visiblePrompts.firstOrNull?.id,
|
||||
// selectedPromptId: prompts.firstOrNull?.id,
|
||||
),
|
||||
);
|
||||
},
|
||||
@@ -139,13 +136,22 @@ class AiPromptSelectorCubit extends Cubit<AiPromptSelectorState> {
|
||||
);
|
||||
}
|
||||
|
||||
AiPrompt? get selectedPrompt {
|
||||
return state.maybeMap(
|
||||
ready: (readyState) {
|
||||
return readyState.visiblePrompts.firstWhereOrNull(
|
||||
(prompt) => prompt.id == readyState.selectedPromptId,
|
||||
);
|
||||
},
|
||||
orElse: () => null,
|
||||
);
|
||||
}
|
||||
|
||||
void _filterTextChanged() {
|
||||
state.maybeMap(
|
||||
ready: (readyState) {
|
||||
final unfilteredPrompts = readyState.selectedCategory == null
|
||||
? readyState.isFeaturedCategorySelected
|
||||
? availablePrompts.where((prompt) => prompt.isFeatured)
|
||||
: availablePrompts
|
||||
? availablePrompts
|
||||
: availablePrompts.where(
|
||||
(prompt) => prompt.category == readyState.selectedCategory,
|
||||
);
|
||||
|
||||
@@ -23,7 +23,7 @@ enum LocalAIStreamingState {
|
||||
}
|
||||
|
||||
abstract class AIRepository {
|
||||
Future<(String, CompletionStream)?> streamCompletion({
|
||||
Future<void> streamCompletion({
|
||||
String? objectId,
|
||||
required String text,
|
||||
PredefinedFormat? format,
|
||||
@@ -45,6 +45,8 @@ abstract class AIRepository {
|
||||
}
|
||||
|
||||
class AppFlowyAIService implements AIRepository {
|
||||
final List<AiPrompt> _builtInPrompts = [];
|
||||
|
||||
@override
|
||||
Future<(String, CompletionStream)?> streamCompletion({
|
||||
String? objectId,
|
||||
@@ -96,6 +98,10 @@ class AppFlowyAIService implements AIRepository {
|
||||
|
||||
@override
|
||||
Future<List<AiPrompt>> getBuiltInPrompts() async {
|
||||
if (_builtInPrompts.isNotEmpty) {
|
||||
return _builtInPrompts;
|
||||
}
|
||||
|
||||
final prompts = <AiPrompt>[];
|
||||
|
||||
try {
|
||||
@@ -114,6 +120,8 @@ class AppFlowyAIService implements AIRepository {
|
||||
Log.error(e);
|
||||
}
|
||||
|
||||
_builtInPrompts.addAll(prompts);
|
||||
|
||||
return prompts;
|
||||
}
|
||||
|
||||
|
||||
@@ -83,7 +83,7 @@ class SelectModelState with _$SelectModelState {
|
||||
}) = _SelectModelState;
|
||||
|
||||
factory SelectModelState.initial(AIModelStateNotifier notifier) {
|
||||
final (models, selectedModel) = notifier.getModelSelection();
|
||||
final (models, selectedModel) = notifier.getAvailableModels();
|
||||
return SelectModelState(
|
||||
models: models,
|
||||
selectedModel: selectedModel,
|
||||
|
||||
+33
-32
@@ -5,7 +5,7 @@ import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class AiPromptCategoryList extends StatefulWidget {
|
||||
class AiPromptCategoryList extends StatelessWidget {
|
||||
const AiPromptCategoryList({
|
||||
super.key,
|
||||
required this.padding,
|
||||
@@ -13,26 +13,19 @@ class AiPromptCategoryList extends StatefulWidget {
|
||||
|
||||
final EdgeInsetsGeometry padding;
|
||||
|
||||
@override
|
||||
State<AiPromptCategoryList> createState() => _AiPromptCategoryListState();
|
||||
}
|
||||
|
||||
class _AiPromptCategoryListState extends State<AiPromptCategoryList> {
|
||||
bool isSearching = false;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = AppFlowyTheme.of(context);
|
||||
|
||||
return Padding(
|
||||
padding: EdgeInsets.all(
|
||||
theme.spacing.l,
|
||||
),
|
||||
padding: padding,
|
||||
child: TextFieldTapRegion(
|
||||
groupId: "ai_prompt_category_list",
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
spacing: theme.spacing.s,
|
||||
children: [
|
||||
_buildSearchField(context),
|
||||
_buildFeaturedCategory(context),
|
||||
const AFDivider(),
|
||||
Expanded(
|
||||
@@ -44,6 +37,7 @@ class _AiPromptCategoryListState extends State<AiPromptCategoryList> {
|
||||
_buildCategoryItem(context, AiPromptCategory.writing),
|
||||
_buildCategoryItem(context, AiPromptCategory.business),
|
||||
_buildCategoryItem(context, AiPromptCategory.marketing),
|
||||
_buildCategoryItem(context, AiPromptCategory.learning),
|
||||
_buildCategoryItem(
|
||||
context,
|
||||
AiPromptCategory.healthAndFitness,
|
||||
@@ -59,15 +53,6 @@ class _AiPromptCategoryListState extends State<AiPromptCategoryList> {
|
||||
_buildCategoryItem(context, AiPromptCategory.strategy),
|
||||
_buildCategoryItem(context, AiPromptCategory.caseStudies),
|
||||
_buildCategoryItem(context, AiPromptCategory.salesCopy),
|
||||
_buildCategoryItem(context, AiPromptCategory.education),
|
||||
_buildCategoryItem(context, AiPromptCategory.work),
|
||||
_buildCategoryItem(
|
||||
context,
|
||||
AiPromptCategory.podcastProduction,
|
||||
),
|
||||
_buildCategoryItem(context, AiPromptCategory.copyWriting),
|
||||
_buildCategoryItem(context, AiPromptCategory.customerSuccess),
|
||||
_buildCategoryItem(context, AiPromptCategory.other),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -77,6 +62,16 @@ class _AiPromptCategoryListState extends State<AiPromptCategoryList> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSearchField(BuildContext context) {
|
||||
return AFTextField(
|
||||
groupId: "ai_prompt_category_list",
|
||||
hintText: "Search",
|
||||
size: AFTextFieldSize.m,
|
||||
controller: context.read<AiPromptSelectorCubit>().filterTextController,
|
||||
autoFocus: true,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCategoryItem(
|
||||
BuildContext context,
|
||||
AiPromptCategory? category,
|
||||
@@ -103,12 +98,15 @@ class _AiPromptCategoryListState extends State<AiPromptCategoryList> {
|
||||
}
|
||||
},
|
||||
builder: (context, isHovering, disabled) {
|
||||
return Text(
|
||||
LocaleKeys.ai_customPrompt_featured.tr(),
|
||||
style: AppFlowyTheme.of(context).textStyle.body.standard(
|
||||
color: theme.textColorScheme.primary,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
return Row(
|
||||
children: [
|
||||
Text(
|
||||
LocaleKeys.ai_customPrompt_featured.tr(),
|
||||
style: AppFlowyTheme.of(context).textStyle.body.standard(
|
||||
color: theme.textColorScheme.primary,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
borderRadius: theme.borderRadius.m,
|
||||
@@ -154,12 +152,15 @@ class AiPromptCategoryItem extends StatelessWidget {
|
||||
return AFBaseButton(
|
||||
onTap: onSelect,
|
||||
builder: (context, isHovering, disabled) {
|
||||
return Text(
|
||||
category?.i18n ?? LocaleKeys.ai_customPrompt_all.tr(),
|
||||
style: AppFlowyTheme.of(context).textStyle.body.standard(
|
||||
color: theme.textColorScheme.primary,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
return Row(
|
||||
children: [
|
||||
Text(
|
||||
category?.i18n ?? LocaleKeys.ai_customPrompt_all.tr(),
|
||||
style: AppFlowyTheme.of(context).textStyle.body.standard(
|
||||
color: theme.textColorScheme.primary,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
borderRadius: theme.borderRadius.m,
|
||||
|
||||
@@ -88,7 +88,12 @@ class AiPromptModal extends StatelessWidget {
|
||||
),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: AiPromptVisibleList(),
|
||||
child: AiPromptVisibleList(
|
||||
padding: EdgeInsets.symmetric(
|
||||
vertical: theme.spacing.l,
|
||||
horizontal: theme.spacing.l,
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
flex: 3,
|
||||
@@ -110,6 +115,10 @@ class AiPromptModal extends StatelessWidget {
|
||||
}
|
||||
return AiPromptPreview(
|
||||
prompt: selectedPrompt,
|
||||
padding: EdgeInsets.symmetric(
|
||||
vertical: theme.spacing.l,
|
||||
horizontal: theme.spacing.l,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
@@ -122,6 +131,20 @@ class AiPromptModal extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
AFModalFooter(
|
||||
trailing: [
|
||||
AFFilledTextButton.primary(
|
||||
text: LocaleKeys.ai_customPrompt_usePrompt.tr(),
|
||||
onTap: () {
|
||||
final selectedPrompt =
|
||||
context.read<AiPromptSelectorCubit>().selectedPrompt;
|
||||
if (selectedPrompt != null) {
|
||||
Navigator.of(context).pop(selectedPrompt);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@@ -10,83 +10,71 @@ class AiPromptPreview extends StatelessWidget {
|
||||
const AiPromptPreview({
|
||||
super.key,
|
||||
required this.prompt,
|
||||
required this.padding,
|
||||
});
|
||||
|
||||
final AiPrompt prompt;
|
||||
final EdgeInsetsGeometry padding;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = AppFlowyTheme.of(context);
|
||||
|
||||
return SelectionArea(
|
||||
child: Column(
|
||||
child: ListView(
|
||||
padding: padding,
|
||||
children: [
|
||||
Padding(
|
||||
padding: EdgeInsets.only(
|
||||
top: theme.spacing.l,
|
||||
left: theme.spacing.l,
|
||||
right: theme.spacing.l,
|
||||
),
|
||||
child: SelectionContainer.disabled(
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
prompt.name,
|
||||
style: theme.textStyle.headline.standard(
|
||||
color: theme.textColorScheme.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
HSpace(theme.spacing.s),
|
||||
AFFilledTextButton.primary(
|
||||
text: LocaleKeys.ai_customPrompt_usePrompt.tr(),
|
||||
onTap: () {
|
||||
Navigator.of(context).pop(prompt);
|
||||
},
|
||||
),
|
||||
],
|
||||
SelectionContainer.disabled(
|
||||
child: Text(
|
||||
prompt.name,
|
||||
style: theme.textStyle.headline.standard(
|
||||
color: theme.textColorScheme.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
VSpace(theme.spacing.xs),
|
||||
Expanded(
|
||||
child: ListView(
|
||||
padding: EdgeInsets.all(
|
||||
theme.spacing.l,
|
||||
SelectionContainer.disabled(
|
||||
child: Text(
|
||||
LocaleKeys.ai_customPrompt_prompt.tr(),
|
||||
style: theme.textStyle.heading4.standard(
|
||||
color: theme.textColorScheme.primary,
|
||||
),
|
||||
children: [
|
||||
SelectionContainer.disabled(
|
||||
child: Text(
|
||||
LocaleKeys.ai_customPrompt_prompt.tr(),
|
||||
style: theme.textStyle.heading4.standard(
|
||||
color: theme.textColorScheme.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
VSpace(theme.spacing.xs),
|
||||
_PromptContent(
|
||||
prompt: prompt,
|
||||
),
|
||||
VSpace(theme.spacing.xl),
|
||||
if (prompt.example.isNotEmpty) ...[
|
||||
SelectionContainer.disabled(
|
||||
child: Text(
|
||||
LocaleKeys.ai_customPrompt_example.tr(),
|
||||
style: theme.textStyle.heading4.standard(
|
||||
color: theme.textColorScheme.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
VSpace(theme.spacing.xs),
|
||||
_PromptExample(
|
||||
prompt: prompt,
|
||||
),
|
||||
VSpace(theme.spacing.xl),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
VSpace(theme.spacing.xs),
|
||||
_PromptContent(
|
||||
prompt: prompt,
|
||||
),
|
||||
VSpace(theme.spacing.xl),
|
||||
if (prompt.example.isNotEmpty) ...[
|
||||
SelectionContainer.disabled(
|
||||
child: Text(
|
||||
LocaleKeys.ai_customPrompt_example.tr(),
|
||||
style: theme.textStyle.heading4.standard(
|
||||
color: theme.textColorScheme.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
VSpace(theme.spacing.xs),
|
||||
_PromptExample(
|
||||
prompt: prompt,
|
||||
),
|
||||
VSpace(theme.spacing.xl),
|
||||
],
|
||||
if (prompt.sampleResponse.isNotEmpty) ...[
|
||||
SelectionContainer.disabled(
|
||||
child: Text(
|
||||
LocaleKeys.ai_customPrompt_sampleOutput.tr(),
|
||||
style: theme.textStyle.heading4.standard(
|
||||
color: theme.textColorScheme.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
VSpace(theme.spacing.xs),
|
||||
_PromptSampleOutput(
|
||||
prompt: prompt,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -187,3 +175,27 @@ class _PromptExample extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PromptSampleOutput extends StatelessWidget {
|
||||
const _PromptSampleOutput({
|
||||
required this.prompt,
|
||||
});
|
||||
|
||||
final AiPrompt prompt;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = AppFlowyTheme.of(context);
|
||||
|
||||
return Container(
|
||||
padding: EdgeInsets.all(theme.spacing.l),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.fillColorScheme.quaternary,
|
||||
borderRadius: BorderRadius.circular(theme.borderRadius.m),
|
||||
),
|
||||
child: AIMarkdownText(
|
||||
markdown: prompt.sampleResponse,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+82
-179
@@ -1,14 +1,9 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:appflowy/ai/ai.dart';
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/util/theme_extension.dart';
|
||||
import 'package:appflowy_ui/appflowy_ui.dart';
|
||||
import 'package:diffutil_dart/diffutil.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/widget/spacing.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
const Duration _listItemAnimationDuration = Duration(milliseconds: 150);
|
||||
@@ -16,8 +11,11 @@ const Duration _listItemAnimationDuration = Duration(milliseconds: 150);
|
||||
class AiPromptVisibleList extends StatefulWidget {
|
||||
const AiPromptVisibleList({
|
||||
super.key,
|
||||
required this.padding,
|
||||
});
|
||||
|
||||
final EdgeInsetsGeometry padding;
|
||||
|
||||
@override
|
||||
State<AiPromptVisibleList> createState() => _AiPromptVisibleListState();
|
||||
}
|
||||
@@ -27,26 +25,18 @@ class _AiPromptVisibleListState extends State<AiPromptVisibleList> {
|
||||
final scrollController = ScrollController();
|
||||
final List<AiPrompt> oldList = [];
|
||||
|
||||
late AiPromptSelectorCubit cubit;
|
||||
late bool filterIsEmpty;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
cubit = context.read<AiPromptSelectorCubit>();
|
||||
final textController = cubit.filterTextController;
|
||||
final prompts = cubit.state.maybeMap(
|
||||
ready: (value) => value.visiblePrompts,
|
||||
orElse: () => <AiPrompt>[],
|
||||
);
|
||||
filterIsEmpty = textController.text.isEmpty;
|
||||
textController.addListener(handleFilterTextChanged);
|
||||
final prompts = context.read<AiPromptSelectorCubit>().state.maybeMap(
|
||||
ready: (value) => value.visiblePrompts,
|
||||
orElse: () => <AiPrompt>[],
|
||||
);
|
||||
oldList.addAll(prompts);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
cubit.filterTextController.removeListener(handleFilterTextChanged);
|
||||
scrollController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
@@ -62,82 +52,34 @@ class _AiPromptVisibleListState extends State<AiPromptVisibleList> {
|
||||
);
|
||||
handleVisiblePromptListChanged(list);
|
||||
},
|
||||
child: Column(
|
||||
children: [
|
||||
VSpace(spacing.l),
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: spacing.l),
|
||||
child: buildSearchField(context),
|
||||
),
|
||||
VSpace(spacing.s),
|
||||
Expanded(
|
||||
child: AnimatedList(
|
||||
controller: scrollController,
|
||||
padding: EdgeInsets.all(spacing.l),
|
||||
key: listKey,
|
||||
initialItemCount: oldList.length,
|
||||
itemBuilder: (context, index, animation) {
|
||||
final cubit = context.read<AiPromptSelectorCubit>();
|
||||
child: AnimatedList(
|
||||
controller: scrollController,
|
||||
padding: widget.padding,
|
||||
key: listKey,
|
||||
initialItemCount: oldList.length,
|
||||
itemBuilder: (context, index, animation) {
|
||||
final cubit = context.read<AiPromptSelectorCubit>();
|
||||
|
||||
return cubit.state.maybeMap(
|
||||
ready: (state) {
|
||||
final prompt = state.visiblePrompts[index];
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(
|
||||
top: index == 0 ? 0 : spacing.s,
|
||||
bottom: index == state.visiblePrompts.length - 1
|
||||
? 0
|
||||
: spacing.s,
|
||||
),
|
||||
child: _AiPromptListItem(
|
||||
animation: animation,
|
||||
prompt: prompt,
|
||||
isSelected: state.selectedPromptId == prompt.id,
|
||||
),
|
||||
);
|
||||
},
|
||||
orElse: () => const SizedBox.shrink(),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildSearchField(BuildContext context) {
|
||||
final theme = AppFlowyTheme.of(context);
|
||||
final iconSize = 20.0;
|
||||
|
||||
return AFTextField(
|
||||
groupId: "ai_prompt_category_list",
|
||||
hintText: "Search",
|
||||
controller: context.read<AiPromptSelectorCubit>().filterTextController,
|
||||
autoFocus: true,
|
||||
suffixIconConstraints: BoxConstraints.tightFor(
|
||||
width: iconSize + theme.spacing.m,
|
||||
height: iconSize,
|
||||
),
|
||||
suffixIconBuilder: filterIsEmpty
|
||||
? null
|
||||
: (context, isObscured) => TextFieldTapRegion(
|
||||
groupId: "ai_prompt_category_list",
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(right: theme.spacing.m),
|
||||
child: GestureDetector(
|
||||
onTap: () => context
|
||||
.read<AiPromptSelectorCubit>()
|
||||
.filterTextController
|
||||
.clear(),
|
||||
child: FlowySvg(
|
||||
FlowySvgs.toast_close_s,
|
||||
color: theme.textColorScheme.secondary,
|
||||
size: const Size.square(20),
|
||||
),
|
||||
),
|
||||
return cubit.state.maybeMap(
|
||||
ready: (state) {
|
||||
final prompt = state.visiblePrompts[index];
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(
|
||||
top: index == 0 ? 0 : spacing.s,
|
||||
bottom:
|
||||
index == state.visiblePrompts.length - 1 ? 0 : spacing.s,
|
||||
),
|
||||
),
|
||||
child: _AiPromptListItem(
|
||||
animation: animation,
|
||||
prompt: prompt,
|
||||
isSelected: state.selectedPromptId == prompt.id,
|
||||
),
|
||||
);
|
||||
},
|
||||
orElse: () => const SizedBox.shrink(),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -180,12 +122,6 @@ class _AiPromptVisibleListState extends State<AiPromptVisibleList> {
|
||||
..clear()
|
||||
..addAll(newList);
|
||||
}
|
||||
|
||||
void handleFilterTextChanged() {
|
||||
setState(() {
|
||||
filterIsEmpty = cubit.filterTextController.text.isEmpty;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class _AiPromptListItem extends StatefulWidget {
|
||||
@@ -210,7 +146,6 @@ class _AiPromptListItemState extends State<_AiPromptListItem> {
|
||||
@override
|
||||
void dispose() {
|
||||
timer?.cancel();
|
||||
timer = null;
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -224,9 +159,6 @@ class _AiPromptListItemState extends State<_AiPromptListItem> {
|
||||
curve: Curves.easeIn,
|
||||
);
|
||||
|
||||
final surfacePrimaryHover =
|
||||
Theme.of(context).isLightMode ? Color(0xFFF8FAFF) : Color(0xFF3C3F4E);
|
||||
|
||||
return FadeTransition(
|
||||
opacity: curvedAnimation,
|
||||
child: SizeTransition(
|
||||
@@ -235,103 +167,74 @@ class _AiPromptListItemState extends State<_AiPromptListItem> {
|
||||
onEnter: (_) {
|
||||
setState(() {
|
||||
isHovering = true;
|
||||
timer = Timer(const Duration(milliseconds: 300), () {
|
||||
if (mounted) {
|
||||
cubit.selectPrompt(widget.prompt.id);
|
||||
}
|
||||
});
|
||||
timer = Timer(
|
||||
const Duration(milliseconds: 300),
|
||||
() {
|
||||
if (isHovering) {
|
||||
cubit.selectPrompt(widget.prompt.id);
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
},
|
||||
onExit: (_) {
|
||||
setState(() {
|
||||
isHovering = false;
|
||||
timer?.cancel();
|
||||
timer = null;
|
||||
});
|
||||
},
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
cubit.selectPrompt(widget.prompt.id);
|
||||
},
|
||||
child: Stack(
|
||||
children: [
|
||||
Container(
|
||||
padding: EdgeInsets.all(theme.spacing.m),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(theme.borderRadius.m),
|
||||
border: Border.all(
|
||||
color: widget.isSelected
|
||||
? isHovering
|
||||
? theme.borderColorScheme.themeThickHover
|
||||
: theme.borderColorScheme.themeThick
|
||||
: isHovering
|
||||
? theme.borderColorScheme.greyTertiaryHover
|
||||
: theme.borderColorScheme.greyTertiary,
|
||||
),
|
||||
color: theme.surfaceColorScheme.primary,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
onDoubleTap: () {
|
||||
Navigator.of(context).pop(widget.prompt);
|
||||
},
|
||||
child: Container(
|
||||
padding: EdgeInsets.all(theme.spacing.m),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(theme.borderRadius.m),
|
||||
border: Border.all(
|
||||
color: widget.isSelected
|
||||
? isHovering
|
||||
? theme.borderColorScheme.themeThickHover
|
||||
: theme.borderColorScheme.themeThick
|
||||
: isHovering
|
||||
? theme.borderColorScheme.greyTertiaryHover
|
||||
: theme.borderColorScheme.greyTertiary,
|
||||
),
|
||||
color: theme.surfaceColorScheme.primary,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
widget.prompt.name,
|
||||
maxLines: 1,
|
||||
style: theme.textStyle.body.standard(
|
||||
color: theme.textColorScheme.primary,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
softWrap: true,
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
widget.prompt.name,
|
||||
maxLines: 1,
|
||||
style: theme.textStyle.body.standard(
|
||||
color: theme.textColorScheme.primary,
|
||||
),
|
||||
],
|
||||
),
|
||||
Text(
|
||||
widget.prompt.content,
|
||||
maxLines: 2,
|
||||
style: theme.textStyle.caption.standard(
|
||||
color: theme.textColorScheme.secondary,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
softWrap: true,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
softWrap: true,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (isHovering)
|
||||
Positioned(
|
||||
top: theme.spacing.s,
|
||||
right: theme.spacing.s,
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(boxShadow: theme.shadow.small),
|
||||
child: AFBaseButton(
|
||||
onTap: () {
|
||||
Navigator.of(context).pop(widget.prompt);
|
||||
},
|
||||
builder: (context, isHovering, disabled) {
|
||||
return Text(
|
||||
LocaleKeys.ai_customPrompt_usePrompt.tr(),
|
||||
style: theme.textStyle.body.standard(
|
||||
color: theme.textColorScheme.primary,
|
||||
),
|
||||
);
|
||||
},
|
||||
backgroundColor: (context, isHovering, disabled) {
|
||||
if (isHovering) {
|
||||
return surfacePrimaryHover;
|
||||
}
|
||||
return theme.surfaceColorScheme.primary;
|
||||
},
|
||||
padding: EdgeInsets.symmetric(
|
||||
vertical: theme.spacing.s,
|
||||
horizontal: theme.spacing.m,
|
||||
),
|
||||
borderRadius: theme.borderRadius.m,
|
||||
),
|
||||
Text(
|
||||
widget.prompt.content,
|
||||
maxLines: 2,
|
||||
style: theme.textStyle.caption.standard(
|
||||
color: theme.textColorScheme.secondary,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
softWrap: true,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -260,12 +260,11 @@ class _DesktopPromptInputState extends State<DesktopPromptInput> {
|
||||
if (widget.isStreaming) {
|
||||
return;
|
||||
}
|
||||
String userInput = widget.textController.text.trim();
|
||||
userInput = inputControlCubit.formatIntputText(userInput);
|
||||
userInput = AiPromptInputTextEditingController.restore(userInput);
|
||||
|
||||
final trimmedText = inputControlCubit.formatIntputText(
|
||||
widget.textController.text.trim(),
|
||||
);
|
||||
widget.textController.clear();
|
||||
if (userInput.isEmpty) {
|
||||
if (trimmedText.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -277,7 +276,7 @@ class _DesktopPromptInputState extends State<DesktopPromptInput> {
|
||||
final predefinedFormat = bloc.state.predefinedFormat;
|
||||
|
||||
widget.onSubmitted(
|
||||
userInput,
|
||||
trimmedText,
|
||||
showPredefinedFormats ? predefinedFormat : null,
|
||||
metadata,
|
||||
);
|
||||
|
||||
+1
-2
@@ -57,8 +57,7 @@ class AiPromptInputTextEditingController extends TextEditingController {
|
||||
text: restore(string),
|
||||
style: theme.textStyle.body.standard().copyWith(
|
||||
color: theme.textColorScheme.purple,
|
||||
backgroundColor:
|
||||
theme.fillColorScheme.purpleThick.withAlpha(51),
|
||||
backgroundColor: theme.fillColorScheme.purpleLight,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -90,40 +90,38 @@ class SelectModelPopoverContent extends StatelessWidget {
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (localModels.isNotEmpty) ...[
|
||||
_ModelSectionHeader(
|
||||
title: LocaleKeys.chat_switchModel_localModel.tr(),
|
||||
),
|
||||
const VSpace(4.0),
|
||||
],
|
||||
...localModels.map(
|
||||
(model) => _ModelItem(
|
||||
model: model,
|
||||
isSelected: model == selectedModel,
|
||||
onTap: () => onSelectModel?.call(model),
|
||||
),
|
||||
),
|
||||
if (cloudModels.isNotEmpty && localModels.isNotEmpty) ...[
|
||||
const VSpace(8.0),
|
||||
_ModelSectionHeader(
|
||||
title: LocaleKeys.chat_switchModel_cloudModel.tr(),
|
||||
),
|
||||
const VSpace(4.0),
|
||||
],
|
||||
...cloudModels.map(
|
||||
(model) => _ModelItem(
|
||||
model: model,
|
||||
isSelected: model == selectedModel,
|
||||
onTap: () => onSelectModel?.call(model),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (localModels.isNotEmpty) ...[
|
||||
_ModelSectionHeader(
|
||||
title: LocaleKeys.chat_switchModel_localModel.tr(),
|
||||
),
|
||||
const VSpace(4.0),
|
||||
],
|
||||
),
|
||||
...localModels.map(
|
||||
(model) => _ModelItem(
|
||||
model: model,
|
||||
isSelected: model == selectedModel,
|
||||
onTap: () => onSelectModel?.call(model),
|
||||
),
|
||||
),
|
||||
if (cloudModels.isNotEmpty && localModels.isNotEmpty) ...[
|
||||
const VSpace(8.0),
|
||||
_ModelSectionHeader(
|
||||
title: LocaleKeys.chat_switchModel_cloudModel.tr(),
|
||||
),
|
||||
const VSpace(4.0),
|
||||
],
|
||||
...cloudModels.map(
|
||||
(model) => _ModelItem(
|
||||
model: model,
|
||||
isSelected: model == selectedModel,
|
||||
onTap: () => onSelectModel?.call(model),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -217,41 +215,45 @@ class _CurrentModelButton extends StatelessWidget {
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: SizedBox(
|
||||
height: DesktopAIPromptSizes.actionBarButtonSize,
|
||||
child: FlowyHover(
|
||||
style: const HoverStyle(
|
||||
borderRadius: BorderRadius.all(Radius.circular(8)),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsetsDirectional.all(4.0),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Padding(
|
||||
// TODO: remove this after change icon to 20px
|
||||
padding: EdgeInsets.all(2),
|
||||
child: FlowySvg(
|
||||
FlowySvgs.ai_sparks_s,
|
||||
color: Theme.of(context).hintColor,
|
||||
size: Size.square(16),
|
||||
),
|
||||
),
|
||||
if (model != null && !model!.isDefault)
|
||||
child: AnimatedSize(
|
||||
duration: const Duration(milliseconds: 50),
|
||||
curve: Curves.easeInOut,
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
child: FlowyHover(
|
||||
style: const HoverStyle(
|
||||
borderRadius: BorderRadius.all(Radius.circular(8)),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsetsDirectional.all(4.0),
|
||||
child: Row(
|
||||
children: [
|
||||
Padding(
|
||||
padding: EdgeInsetsDirectional.only(end: 2.0),
|
||||
child: FlowyText(
|
||||
model!.i18n,
|
||||
fontSize: 12,
|
||||
figmaLineHeight: 16,
|
||||
// TODO: remove this after change icon to 20px
|
||||
padding: EdgeInsets.all(2),
|
||||
child: FlowySvg(
|
||||
FlowySvgs.ai_sparks_s,
|
||||
color: Theme.of(context).hintColor,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
size: Size.square(16),
|
||||
),
|
||||
),
|
||||
FlowySvg(
|
||||
FlowySvgs.ai_source_drop_down_s,
|
||||
color: Theme.of(context).hintColor,
|
||||
size: const Size.square(8),
|
||||
),
|
||||
],
|
||||
if (model != null && !model!.isDefault)
|
||||
Padding(
|
||||
padding: EdgeInsetsDirectional.only(end: 2.0),
|
||||
child: FlowyText(
|
||||
model!.i18n,
|
||||
fontSize: 12,
|
||||
figmaLineHeight: 16,
|
||||
color: Theme.of(context).hintColor,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
FlowySvg(
|
||||
FlowySvgs.ai_source_drop_down_s,
|
||||
color: Theme.of(context).hintColor,
|
||||
size: const Size.square(8),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
+1
-1
@@ -54,7 +54,7 @@ class _PromptInputMobileSelectSourcesButtonState
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<UserWorkspaceBloc, UserWorkspaceState>(
|
||||
builder: (context, state) {
|
||||
final userProfile = context.read<UserWorkspaceBloc>().state.userProfile;
|
||||
final userProfile = context.read<UserWorkspaceBloc>().userProfile;
|
||||
final workspaceId = state.currentWorkspace?.workspaceId ?? '';
|
||||
return MultiBlocProvider(
|
||||
providers: [
|
||||
|
||||
@@ -60,7 +60,7 @@ class _PromptInputDesktopSelectSourcesButtonState
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final userWorkspaceBloc = context.read<UserWorkspaceBloc>();
|
||||
final userProfile = userWorkspaceBloc.state.userProfile;
|
||||
final userProfile = userWorkspaceBloc.userProfile;
|
||||
final workspaceId =
|
||||
userWorkspaceBloc.state.currentWorkspace?.workspaceId ?? '';
|
||||
|
||||
|
||||
@@ -39,11 +39,8 @@ Future<bool> afLaunchUri(
|
||||
);
|
||||
}
|
||||
|
||||
// on Linux or Android or Windows, add http scheme to the url if it is not present
|
||||
if ((UniversalPlatform.isLinux ||
|
||||
UniversalPlatform.isAndroid ||
|
||||
UniversalPlatform.isWindows) &&
|
||||
!isURL(url, {'require_protocol': true})) {
|
||||
// on Linux, add http scheme to the url if it is not present
|
||||
if (UniversalPlatform.isLinux && !isURL(url, {'require_protocol': true})) {
|
||||
uri = Uri.parse('https://$url');
|
||||
}
|
||||
|
||||
|
||||
+1
-6
@@ -167,16 +167,11 @@ Future<void> useBaseWebDomain(String? url) async {
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> useSelfHostedAppFlowyCloud(String url) async {
|
||||
Future<void> useSelfHostedAppFlowyCloudWithURL(String url) async {
|
||||
await _setAuthenticatorType(AuthenticatorType.appflowyCloudSelfHost);
|
||||
await _setAppFlowyCloudUrl(url);
|
||||
}
|
||||
|
||||
Future<void> useAppFlowyCloudDevelop(String url) async {
|
||||
await _setAuthenticatorType(AuthenticatorType.appflowyCloudDevelop);
|
||||
await _setAppFlowyCloudUrl(url);
|
||||
}
|
||||
|
||||
Future<void> useAppFlowyBetaCloudWithURL(
|
||||
String url,
|
||||
AuthenticatorType authenticatorType,
|
||||
|
||||
+17
-14
@@ -30,7 +30,7 @@ class NotificationReminderBloc
|
||||
add(const NotificationReminderEvent.reset());
|
||||
},
|
||||
reset: () async {
|
||||
final scheduledAt = await _getScheduledAt(
|
||||
final createdAt = await _getCreatedAt(
|
||||
reminder,
|
||||
dateFormat,
|
||||
timeFormat,
|
||||
@@ -40,24 +40,23 @@ class NotificationReminderBloc
|
||||
if (view == null) {
|
||||
emit(
|
||||
NotificationReminderState(
|
||||
scheduledAt: scheduledAt,
|
||||
createdAt: createdAt,
|
||||
pageTitle: '',
|
||||
reminderContent: '',
|
||||
isLocked: false,
|
||||
status: NotificationReminderStatus.error,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final layout = view.layout;
|
||||
final layout = view!.layout;
|
||||
|
||||
if (layout.isDocumentView) {
|
||||
final node = await _getContent(reminder);
|
||||
if (node != null) {
|
||||
emit(
|
||||
NotificationReminderState(
|
||||
scheduledAt: scheduledAt,
|
||||
createdAt: createdAt,
|
||||
pageTitle: view.nameOrDefault,
|
||||
isLocked: view.isLocked,
|
||||
view: view,
|
||||
@@ -71,7 +70,7 @@ class NotificationReminderBloc
|
||||
} else if (layout.isDatabaseView) {
|
||||
emit(
|
||||
NotificationReminderState(
|
||||
scheduledAt: scheduledAt,
|
||||
createdAt: createdAt,
|
||||
pageTitle: view.nameOrDefault,
|
||||
isLocked: view.isLocked,
|
||||
view: view,
|
||||
@@ -89,16 +88,20 @@ class NotificationReminderBloc
|
||||
late final UserDateFormatPB dateFormat;
|
||||
late final UserTimeFormatPB timeFormat;
|
||||
|
||||
Future<String> _getScheduledAt(
|
||||
Future<String> _getCreatedAt(
|
||||
ReminderPB reminder,
|
||||
UserDateFormatPB dateFormat,
|
||||
UserTimeFormatPB timeFormat,
|
||||
) async {
|
||||
return _formatTimestamp(
|
||||
reminder.scheduledAt.toInt() * 1000,
|
||||
timeFormat: timeFormat,
|
||||
dateFormate: dateFormat,
|
||||
);
|
||||
final rCreatedAt = reminder.createdAt;
|
||||
final createdAt = rCreatedAt != null
|
||||
? _formatTimestamp(
|
||||
rCreatedAt,
|
||||
timeFormat: timeFormat,
|
||||
dateFormate: dateFormat,
|
||||
)
|
||||
: '';
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
Future<ViewPB?> _getView(ReminderPB reminder) async {
|
||||
@@ -200,7 +203,7 @@ class NotificationReminderState with _$NotificationReminderState {
|
||||
const NotificationReminderState._();
|
||||
|
||||
const factory NotificationReminderState({
|
||||
required String scheduledAt,
|
||||
required String createdAt,
|
||||
required String pageTitle,
|
||||
required String reminderContent,
|
||||
required bool isLocked,
|
||||
@@ -213,7 +216,7 @@ class NotificationReminderState with _$NotificationReminderState {
|
||||
|
||||
factory NotificationReminderState.initial() =>
|
||||
const NotificationReminderState(
|
||||
scheduledAt: '',
|
||||
createdAt: '',
|
||||
pageTitle: '',
|
||||
reminderContent: '',
|
||||
isLocked: false,
|
||||
|
||||
+1
-11
@@ -53,7 +53,6 @@ class _MobileBottomSheetEditLinkWidgetState
|
||||
ViewPB? currentView;
|
||||
bool showErrorText = false;
|
||||
bool showRemoveLink = false;
|
||||
String title = LocaleKeys.editor_editLink.tr();
|
||||
|
||||
AppFlowyThemeData get theme => AppFlowyTheme.of(context);
|
||||
|
||||
@@ -74,7 +73,6 @@ class _MobileBottomSheetEditLinkWidgetState
|
||||
)..searchRecentViews();
|
||||
if (linkInfo.link.isEmpty) {
|
||||
isShowingSearchResult = true;
|
||||
title = LocaleKeys.toolbar_addLink.tr();
|
||||
} else {
|
||||
showRemoveLink = true;
|
||||
textFocusNode.requestFocus();
|
||||
@@ -106,7 +104,7 @@ class _MobileBottomSheetEditLinkWidgetState
|
||||
child: Column(
|
||||
children: [
|
||||
BottomSheetHeader(
|
||||
title: title,
|
||||
title: LocaleKeys.editor_editLink.tr(),
|
||||
onClose: () => context.pop(),
|
||||
confirmButton: FlowyTextButton(
|
||||
LocaleKeys.button_done.tr(),
|
||||
@@ -165,7 +163,6 @@ class _MobileBottomSheetEditLinkWidgetState
|
||||
decoration: LinkStyle.buildLinkTextFieldInputDecoration(
|
||||
LocaleKeys.document_toolbar_linkNameHint.tr(),
|
||||
contentPadding: EdgeInsets.all(14),
|
||||
radius: 12,
|
||||
context,
|
||||
),
|
||||
),
|
||||
@@ -189,11 +186,6 @@ class _MobileBottomSheetEditLinkWidgetState
|
||||
autofocus: true,
|
||||
context: context,
|
||||
contentPadding: EdgeInsets.all(14),
|
||||
textStyle: TextStyle(
|
||||
fontSize: 16,
|
||||
height: 20 / 16,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
),
|
||||
VSpace(6),
|
||||
searchTextField.buildResultContainer(
|
||||
@@ -275,7 +267,6 @@ class _MobileBottomSheetEditLinkWidgetState
|
||||
height: 48,
|
||||
decoration: buildBorderDecoration(),
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: showSearchResult,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(12),
|
||||
@@ -300,7 +291,6 @@ class _MobileBottomSheetEditLinkWidgetState
|
||||
Widget buildRemoveLink() {
|
||||
if (!showRemoveLink) return SizedBox.shrink();
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () {
|
||||
widget.onRemoveLink(linkInfo);
|
||||
context.pop();
|
||||
|
||||
+1
-1
@@ -203,7 +203,7 @@ class MobileViewBottomSheetBody extends StatelessWidget {
|
||||
final userProfile = context.read<MobileViewPageBloc>().state.userProfilePB;
|
||||
// the publish feature is only available for AppFlowy Cloud
|
||||
if (userProfile == null ||
|
||||
userProfile.workspaceType != WorkspaceTypePB.ServerW) {
|
||||
userProfile.workspaceAuthType != AuthTypePB.Server) {
|
||||
return [];
|
||||
}
|
||||
|
||||
|
||||
+4
-6
@@ -1,6 +1,5 @@
|
||||
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet_buttons.dart';
|
||||
import 'package:appflowy/plugins/base/drag_handler.dart';
|
||||
import 'package:appflowy_ui/appflowy_ui.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
@@ -236,7 +235,6 @@ class BottomSheetHeader extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = AppFlowyTheme.of(context);
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 4.0),
|
||||
child: SizedBox(
|
||||
@@ -267,11 +265,11 @@ class BottomSheetHeader extends StatelessWidget {
|
||||
Align(
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(maxWidth: 250),
|
||||
child: Text(
|
||||
child: FlowyText(
|
||||
title,
|
||||
style: theme.textStyle.heading4.prominent(
|
||||
color: theme.textColorScheme.primary,
|
||||
),
|
||||
fontSize: 17.0,
|
||||
fontWeight: FontWeight.w500,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -194,7 +194,7 @@ class _MobileWorkspace extends StatelessWidget {
|
||||
context.read<UserWorkspaceBloc>().add(
|
||||
UserWorkspaceEvent.openWorkspace(
|
||||
workspace.workspaceId,
|
||||
workspace.workspaceType,
|
||||
workspace.workspaceAuthType,
|
||||
),
|
||||
);
|
||||
},
|
||||
|
||||
+1
-2
@@ -85,8 +85,7 @@ class _MobileHomeSettingPageState extends State<MobileHomeSettingPage> {
|
||||
PersonalInfoSettingGroup(
|
||||
userProfile: userProfile,
|
||||
),
|
||||
if (state.userProfile.userAuthType == AuthTypePB.Server)
|
||||
const WorkspaceSettingGroup(),
|
||||
const WorkspaceSettingGroup(),
|
||||
const AppearanceSettingGroup(),
|
||||
const LanguageSettingGroup(),
|
||||
if (Env.enableCustomCloud) const CloudSettingGroup(),
|
||||
|
||||
+1
-1
@@ -48,7 +48,7 @@ class HomePageSettingsPopupMenu extends StatelessWidget {
|
||||
text: LocaleKeys.settings_popupMenuItem_settings.tr(),
|
||||
),
|
||||
// only show the member items in cloud mode
|
||||
if (userProfile.workspaceType == WorkspaceTypePB.ServerW) ...[
|
||||
if (userProfile.workspaceAuthType == AuthTypePB.Server) ...[
|
||||
const PopupMenuDivider(height: 0.5),
|
||||
_buildItem(
|
||||
value: _MobileSettingsPopupMenuItem.members,
|
||||
|
||||
@@ -200,9 +200,7 @@ class MobileViewPage extends StatelessWidget {
|
||||
const WidgetSpan(child: HSpace(8.0)),
|
||||
],
|
||||
TextSpan(
|
||||
text: name.orDefault(
|
||||
LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
|
||||
),
|
||||
text: name,
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
fontSize: 16.0,
|
||||
fontWeight: FontWeight.w600,
|
||||
|
||||
@@ -164,7 +164,7 @@ class _MobileSpaceTabState extends State<MobileSpaceTab>
|
||||
return const MobileRecentSpace();
|
||||
case MobileSpaceTabType.spaces:
|
||||
final showAIFloatingButton =
|
||||
widget.userProfile.workspaceType == WorkspaceTypePB.ServerW;
|
||||
widget.userProfile.workspaceAuthType == AuthTypePB.Server;
|
||||
return Stack(
|
||||
children: [
|
||||
MobileHomeSpace(userProfile: widget.userProfile),
|
||||
|
||||
+1
-1
@@ -123,7 +123,7 @@ class _CreateWorkspaceButton extends StatelessWidget {
|
||||
context.read<UserWorkspaceBloc>().add(
|
||||
UserWorkspaceEvent.createWorkspace(
|
||||
name,
|
||||
WorkspaceTypePB.ServerW,
|
||||
AuthTypePB.Server,
|
||||
),
|
||||
);
|
||||
},
|
||||
|
||||
+17
-22
@@ -6,10 +6,10 @@ import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/mobile/presentation/notifications/mobile_notifications_screen.dart';
|
||||
import 'package:appflowy/mobile/presentation/widgets/navigation_bar_button.dart';
|
||||
import 'package:appflowy/shared/popup_menu/appflowy_popup_menu.dart';
|
||||
import 'package:appflowy/shared/red_dot.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/user/application/reminder/reminder_bloc.dart';
|
||||
import 'package:appflowy/util/theme_extension.dart';
|
||||
import 'package:appflowy/workspace/presentation/notifications/number_red_dot.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
@@ -175,28 +175,23 @@ class _NotificationNavigationBarItemIcon extends StatelessWidget {
|
||||
final hasUnreads = state.reminders.any(
|
||||
(reminder) => !reminder.isRead,
|
||||
);
|
||||
return SizedBox(
|
||||
width: 40,
|
||||
height: 40,
|
||||
child: Stack(
|
||||
children: [
|
||||
Center(
|
||||
child: isActive
|
||||
? const FlowySvg(
|
||||
FlowySvgs.m_home_active_notification_m,
|
||||
blendMode: null,
|
||||
)
|
||||
: const FlowySvg(
|
||||
FlowySvgs.m_home_notification_m,
|
||||
),
|
||||
return Stack(
|
||||
children: [
|
||||
isActive
|
||||
? const FlowySvg(
|
||||
FlowySvgs.m_home_active_notification_m,
|
||||
blendMode: null,
|
||||
)
|
||||
: const FlowySvg(
|
||||
FlowySvgs.m_home_notification_m,
|
||||
),
|
||||
if (hasUnreads)
|
||||
const Positioned(
|
||||
top: 2,
|
||||
right: 4,
|
||||
child: NotificationRedDot(),
|
||||
),
|
||||
if (hasUnreads)
|
||||
const Align(
|
||||
alignment: Alignment.topRight,
|
||||
child: NumberedRedDot.mobile(),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
+1
@@ -126,6 +126,7 @@ class _NotificationScreenContent extends StatelessWidget {
|
||||
onAction: _onAction,
|
||||
onReadChanged: _onReadChanged,
|
||||
actionBar: InboxActionBar(
|
||||
hasUnreads: state.hasUnreads,
|
||||
showUnreadsOnly: filterState.showUnreadsOnly,
|
||||
),
|
||||
),
|
||||
|
||||
+3
-1
@@ -101,7 +101,9 @@ class _InnerNotificationItem extends StatelessWidget {
|
||||
NotificationCheckIcon(
|
||||
isSelected: mSelectedNotificationIds.value.contains(reminder.id),
|
||||
),
|
||||
const HSpace(12.0),
|
||||
const HSpace(3.0),
|
||||
!reminder.isRead ? const UnreadRedDot() : const HSpace(6.0),
|
||||
const HSpace(3.0),
|
||||
NotificationIcon(reminder: reminder),
|
||||
const HSpace(12.0),
|
||||
Expanded(
|
||||
|
||||
+12
-14
@@ -48,19 +48,14 @@ class NotificationItem extends StatelessWidget {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
final child = GestureDetector(
|
||||
onLongPress: () {
|
||||
context.onMoreAction();
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: _SlidableNotificationItem(
|
||||
final child = Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: _SlidableNotificationItem(
|
||||
tabType: tabType,
|
||||
reminder: reminder,
|
||||
child: _InnerNotificationItem(
|
||||
tabType: tabType,
|
||||
reminder: reminder,
|
||||
child: _InnerNotificationItem(
|
||||
tabType: tabType,
|
||||
reminder: reminder,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -107,11 +102,14 @@ class _InnerNotificationItem extends StatelessWidget {
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const HSpace(10.0),
|
||||
const HSpace(8.0),
|
||||
!reminder.isRead ? const UnreadRedDot() : const HSpace(6.0),
|
||||
const HSpace(4.0),
|
||||
NotificationIcon(reminder: reminder),
|
||||
const HSpace(12.0),
|
||||
Expanded(child: NotificationContent(reminder: reminder)),
|
||||
const HSpace(6.0),
|
||||
Expanded(
|
||||
child: NotificationContent(reminder: reminder),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
+28
-36
@@ -10,11 +10,11 @@ import 'package:appflowy/workspace/application/view/view_ext.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:appflowy_ui/appflowy_ui.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:universal_platform/universal_platform.dart';
|
||||
|
||||
const _kNotificationIconHeight = 36.0;
|
||||
|
||||
@@ -22,15 +22,12 @@ class NotificationIcon extends StatelessWidget {
|
||||
const NotificationIcon({
|
||||
super.key,
|
||||
required this.reminder,
|
||||
this.atSize = 12,
|
||||
});
|
||||
|
||||
final ReminderPB reminder;
|
||||
final double atSize;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = AppFlowyTheme.of(context);
|
||||
return SizedBox(
|
||||
width: 42,
|
||||
height: 36,
|
||||
@@ -48,13 +45,13 @@ class NotificationIcon extends StatelessWidget {
|
||||
height: 20,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: theme.fillColorScheme.primary,
|
||||
color: Theme.of(context).cardColor,
|
||||
),
|
||||
child: Center(
|
||||
child: FlowySvg(
|
||||
FlowySvgs.notification_icon_at_s,
|
||||
size: Size.square(atSize),
|
||||
color: theme.iconColorScheme.primary,
|
||||
child: FlowyText(
|
||||
'@',
|
||||
fontSize: 12,
|
||||
figmaLineHeight: 14,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -91,15 +88,14 @@ class UnreadRedDot extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = AppFlowyTheme.of(context);
|
||||
return SizedBox(
|
||||
return const SizedBox(
|
||||
height: _kNotificationIconHeight,
|
||||
child: Center(
|
||||
child: SizedBox.square(
|
||||
dimension: 7.0,
|
||||
dimension: 6.0,
|
||||
child: DecoratedBox(
|
||||
decoration: ShapeDecoration(
|
||||
color: theme.borderColorScheme.errorThick,
|
||||
color: Color(0xFFFF6331),
|
||||
shape: OvalBorder(),
|
||||
),
|
||||
),
|
||||
@@ -122,8 +118,6 @@ class NotificationContent extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _NotificationContentState extends State<NotificationContent> {
|
||||
AppFlowyThemeData get theme => AppFlowyTheme.of(context);
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant NotificationContent oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
@@ -147,11 +141,14 @@ class _NotificationContentState extends State<NotificationContent> {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// title & time
|
||||
_buildHeader(state.scheduledAt, !widget.reminder.isRead),
|
||||
_buildHeader(state.createdAt, !widget.reminder.isRead),
|
||||
// page name
|
||||
_buildPageName(context, state.isLocked, state.pageTitle),
|
||||
// content
|
||||
_buildContent(view, nodes: state.nodes),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 16.0),
|
||||
child: _buildContent(view, nodes: state.nodes),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
@@ -178,8 +175,6 @@ class _NotificationContentState extends State<NotificationContent> {
|
||||
fontSize: 14,
|
||||
figmaLineHeight: 22,
|
||||
color: context.notificationItemTextColor,
|
||||
maxLines: 3,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -196,7 +191,6 @@ class _NotificationContentState extends State<NotificationContent> {
|
||||
LocaleKeys.settings_notifications_titles_reminder.tr(),
|
||||
fontSize: 14,
|
||||
figmaLineHeight: 20,
|
||||
color: theme.textColorScheme.primary,
|
||||
),
|
||||
Spacer(),
|
||||
if (createAt.isNotEmpty)
|
||||
@@ -204,7 +198,7 @@ class _NotificationContentState extends State<NotificationContent> {
|
||||
createAt,
|
||||
fontSize: 12,
|
||||
figmaLineHeight: 18,
|
||||
color: theme.textColorScheme.secondary,
|
||||
color: context.notificationItemTextColor,
|
||||
),
|
||||
if (unread) ...[
|
||||
HSpace(4),
|
||||
@@ -231,25 +225,19 @@ class _NotificationContentState extends State<NotificationContent> {
|
||||
LocaleKeys.notificationHub_mentionedYou.tr(),
|
||||
fontSize: 12,
|
||||
figmaLineHeight: 18,
|
||||
color: theme.textColorScheme.secondary,
|
||||
color: context.notificationItemTextColor,
|
||||
),
|
||||
const NotificationEllipse(),
|
||||
if (isLocked)
|
||||
Padding(
|
||||
padding: EdgeInsets.only(right: 5),
|
||||
child: FlowySvg(
|
||||
FlowySvgs.notification_lock_s,
|
||||
color: theme.iconColorScheme.secondary,
|
||||
),
|
||||
),
|
||||
Flexible(
|
||||
child: FlowyText.regular(
|
||||
pageTitle,
|
||||
fontSize: 12,
|
||||
figmaLineHeight: 18,
|
||||
color: theme.textColorScheme.secondary,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
child: FlowySvg(FlowySvgs.notification_lock_s),
|
||||
),
|
||||
FlowyText.regular(
|
||||
pageTitle,
|
||||
fontSize: 12,
|
||||
figmaLineHeight: 18,
|
||||
color: context.notificationItemTextColor,
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -266,7 +254,7 @@ class NotificationEllipse extends StatelessWidget {
|
||||
return Container(
|
||||
width: 2.50,
|
||||
height: 2.50,
|
||||
margin: const EdgeInsets.symmetric(horizontal: 6.0),
|
||||
margin: const EdgeInsets.symmetric(horizontal: 5.0),
|
||||
decoration: ShapeDecoration(
|
||||
color: context.notificationItemTextColor,
|
||||
shape: const OvalBorder(),
|
||||
@@ -322,7 +310,11 @@ class NotificationDocumentContent extends StatelessWidget {
|
||||
styleCustomizer: styleCustomizer,
|
||||
// the editor is not editable in the chat
|
||||
editable: false,
|
||||
customPadding: (node) => EdgeInsets.zero,
|
||||
customHeadingPadding: UniversalPlatform.isDesktop
|
||||
? EdgeInsets.zero
|
||||
: EdgeInsets.symmetric(
|
||||
vertical: EditorStyleCustomizer.nodeHorizontalPadding,
|
||||
),
|
||||
);
|
||||
|
||||
return IgnorePointer(
|
||||
|
||||
+30
-32
@@ -76,43 +76,41 @@ enum NotificationPaneActionType {
|
||||
topLeft: Radius.circular(10),
|
||||
bottomLeft: Radius.circular(10),
|
||||
),
|
||||
onPressed: (context) => context.onMoreAction(),
|
||||
onPressed: (context) {
|
||||
final reminderBloc = context.read<ReminderBloc>();
|
||||
final notificationReminderBloc =
|
||||
context.read<NotificationReminderBloc>();
|
||||
|
||||
showMobileBottomSheet(
|
||||
context,
|
||||
showDragHandle: true,
|
||||
showDivider: false,
|
||||
useRootNavigator: true,
|
||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||
builder: (_) {
|
||||
return MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider.value(value: reminderBloc),
|
||||
BlocProvider.value(value: notificationReminderBloc),
|
||||
],
|
||||
child: _NotificationMoreActions(
|
||||
onClickMultipleChoice: () {
|
||||
Future.delayed(const Duration(milliseconds: 250), () {
|
||||
bottomNavigationBarType.value =
|
||||
BottomNavigationBarActionType
|
||||
.notificationMultiSelect;
|
||||
});
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension NotificationPaneActionExtension on BuildContext {
|
||||
void onMoreAction() {
|
||||
final reminderBloc = read<ReminderBloc>();
|
||||
final notificationReminderBloc = read<NotificationReminderBloc>();
|
||||
|
||||
showMobileBottomSheet(
|
||||
this,
|
||||
showDragHandle: true,
|
||||
showDivider: false,
|
||||
useRootNavigator: true,
|
||||
backgroundColor: Theme.of(this).colorScheme.surface,
|
||||
builder: (_) {
|
||||
return MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider.value(value: reminderBloc),
|
||||
BlocProvider.value(value: notificationReminderBloc),
|
||||
],
|
||||
child: _NotificationMoreActions(
|
||||
onClickMultipleChoice: () {
|
||||
Future.delayed(const Duration(milliseconds: 250), () {
|
||||
bottomNavigationBarType.value =
|
||||
BottomNavigationBarActionType.notificationMultiSelect;
|
||||
});
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _NotificationMoreActions extends StatelessWidget {
|
||||
const _NotificationMoreActions({
|
||||
required this.onClickMultipleChoice,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/workspace/application/command_palette/command_palette_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/command_palette/search_result_ext.dart';
|
||||
@@ -22,18 +23,12 @@ class MobileSearchResultCell extends StatelessWidget {
|
||||
final theme = AppFlowyTheme.of(context),
|
||||
textColor = theme.textColorScheme.primary;
|
||||
final commandPaletteState = context.read<CommandPaletteBloc>().state;
|
||||
final displayName = item.displayName.isEmpty
|
||||
? LocaleKeys.menuAppHeader_defaultNewPageName.tr()
|
||||
: item.displayName;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 12),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox.square(
|
||||
dimension: 24,
|
||||
child: Center(child: buildIcon(theme)),
|
||||
),
|
||||
buildIcon(),
|
||||
HSpace(12),
|
||||
Flexible(
|
||||
child: Column(
|
||||
@@ -43,7 +38,7 @@ class MobileSearchResultCell extends StatelessWidget {
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
text: buildHighLightSpan(
|
||||
content: displayName,
|
||||
content: item.displayName,
|
||||
normal: theme.textStyle.heading4.standard(color: textColor),
|
||||
highlight: theme.textStyle.heading4
|
||||
.standard(color: textColor)
|
||||
@@ -53,7 +48,7 @@ class MobileSearchResultCell extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
buildPath(commandPaletteState, theme),
|
||||
...buildSummary(theme),
|
||||
buildSummary(theme),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -62,55 +57,74 @@ class MobileSearchResultCell extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildIcon(AppFlowyThemeData theme) {
|
||||
Widget buildIcon() {
|
||||
final icon = item.icon;
|
||||
if (icon.ty == ResultIconTypePB.Emoji) {
|
||||
return icon.getIcon(size: 20) ?? SizedBox.shrink();
|
||||
return icon.getIcon(size: 16.0, lineHeight: 20 / 16) ?? SizedBox.shrink();
|
||||
} else {
|
||||
return icon.getIcon(size: 20, iconColor: theme.iconColorScheme.primary) ??
|
||||
SizedBox.shrink();
|
||||
return icon.getIcon(size: 20) ?? SizedBox.shrink();
|
||||
}
|
||||
}
|
||||
|
||||
Widget buildPath(CommandPaletteState state, AppFlowyThemeData theme) {
|
||||
return BlocProvider(
|
||||
create: (context) => ViewAncestorBloc(item.id),
|
||||
child: BlocBuilder<ViewAncestorBloc, ViewAncestorState>(
|
||||
builder: (context, state) {
|
||||
final ancestors = state.ancestor.ancestors;
|
||||
List<String> displayPath = ancestors.map((e) => e.name).toList();
|
||||
if (ancestors.length > 2) {
|
||||
displayPath = [ancestors.first.name, '...', ancestors.last.name];
|
||||
}
|
||||
return Text(
|
||||
displayPath.join(' / '),
|
||||
bool isInTrash = false;
|
||||
for (final view in state.trash) {
|
||||
if (view.id == item.id) {
|
||||
isInTrash = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (isInTrash) {
|
||||
return Row(
|
||||
children: [
|
||||
const FlowySvg(FlowySvgs.trash_s, size: Size.square(20)),
|
||||
const HSpace(4.0),
|
||||
Text(
|
||||
'${LocaleKeys.trash_text.tr()} / ${item.displayName}',
|
||||
style: theme.textStyle.body
|
||||
.standard(color: theme.textColorScheme.secondary),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
),
|
||||
],
|
||||
);
|
||||
} else {
|
||||
return BlocProvider(
|
||||
create: (context) => ViewAncestorBloc(item.id),
|
||||
child: BlocBuilder<ViewAncestorBloc, ViewAncestorState>(
|
||||
builder: (context, state) {
|
||||
final ancestors = state.ancestor.ancestors;
|
||||
List<String> displayPath = ancestors.map((e) => e.name).toList();
|
||||
if (ancestors.length > 2) {
|
||||
displayPath = [ancestors.first.name, '...', ancestors.last.name];
|
||||
}
|
||||
return Text(
|
||||
displayPath.join(' / '),
|
||||
style: theme.textStyle.body
|
||||
.standard(color: theme.textColorScheme.secondary),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
List<Widget> buildSummary(AppFlowyThemeData theme) {
|
||||
if (item.content.isEmpty) return [];
|
||||
return [
|
||||
VSpace(theme.spacing.m),
|
||||
RichText(
|
||||
maxLines: 3,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
text: buildHighLightSpan(
|
||||
content: item.content,
|
||||
normal: theme.textStyle.heading4
|
||||
.standard(color: theme.textColorScheme.secondary),
|
||||
highlight: theme.textStyle.heading4
|
||||
.standard(color: theme.textColorScheme.primary)
|
||||
.copyWith(
|
||||
backgroundColor: theme.fillColorScheme.themeSelect,
|
||||
),
|
||||
),
|
||||
Widget buildSummary(AppFlowyThemeData theme) {
|
||||
if (item.content.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
return RichText(
|
||||
maxLines: 3,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
text: buildHighLightSpan(
|
||||
content: item.content,
|
||||
normal: theme.textStyle.heading4
|
||||
.standard(color: theme.textColorScheme.secondary),
|
||||
highlight: theme.textStyle.heading4
|
||||
.standard(color: theme.textColorScheme.primary)
|
||||
.copyWith(
|
||||
backgroundColor: theme.fillColorScheme.themeSelect,
|
||||
),
|
||||
),
|
||||
];
|
||||
);
|
||||
}
|
||||
|
||||
TextSpan buildHighLightSpan({
|
||||
|
||||
@@ -61,11 +61,16 @@ class MobileSearchScreen extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
);
|
||||
return Provider.value(
|
||||
value: userProfile,
|
||||
child: MobileSearchPage(
|
||||
userProfile: userProfile,
|
||||
workspaceLatestPB: latest,
|
||||
return Scaffold(
|
||||
body: SafeArea(
|
||||
bottom: false,
|
||||
child: Provider.value(
|
||||
value: userProfile,
|
||||
child: MobileSearchPage(
|
||||
userProfile: userProfile,
|
||||
workspaceLatestPB: latest,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
@@ -73,7 +78,7 @@ class MobileSearchScreen extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class MobileSearchPage extends StatefulWidget {
|
||||
class MobileSearchPage extends StatelessWidget {
|
||||
const MobileSearchPage({
|
||||
super.key,
|
||||
required this.userProfile,
|
||||
@@ -83,62 +88,31 @@ class MobileSearchPage extends StatefulWidget {
|
||||
final UserProfilePB userProfile;
|
||||
final WorkspaceLatestPB workspaceLatestPB;
|
||||
|
||||
@override
|
||||
State<MobileSearchPage> createState() => _MobileSearchPageState();
|
||||
}
|
||||
|
||||
class _MobileSearchPageState extends State<MobileSearchPage> {
|
||||
bool get enableShowAISearch =>
|
||||
widget.userProfile.workspaceType == WorkspaceTypePB.ServerW;
|
||||
|
||||
final focusNode = FocusNode();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
focusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
userProfile.workspaceAuthType == AuthTypePB.Server;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<CommandPaletteBloc, CommandPaletteState>(
|
||||
builder: (context, state) {
|
||||
return SafeArea(
|
||||
child: Scaffold(
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MobileSearchTextfield(
|
||||
focusNode: focusNode,
|
||||
hintText: enableShowAISearch
|
||||
? LocaleKeys.search_searchOrAskAI.tr()
|
||||
: LocaleKeys.search_label.tr(),
|
||||
query: state.query ?? '',
|
||||
onChanged: (value) =>
|
||||
context.read<CommandPaletteBloc>().add(
|
||||
CommandPaletteEvent.searchChanged(search: value),
|
||||
),
|
||||
),
|
||||
if (enableShowAISearch)
|
||||
MobileSearchAskAiEntrance(query: state.query),
|
||||
Flexible(
|
||||
child: NotificationListener(
|
||||
child: MobileSearchResult(),
|
||||
onNotification: (t) {
|
||||
if (t is ScrollUpdateNotification) {
|
||||
if (focusNode.hasFocus) {
|
||||
focusNode.unfocus();
|
||||
}
|
||||
}
|
||||
return true;
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MobileSearchTextfield(
|
||||
hintText: enableShowAISearch
|
||||
? LocaleKeys.search_searchOrAskAI.tr()
|
||||
: LocaleKeys.search_label.tr(),
|
||||
query: state.query ?? '',
|
||||
onChanged: (value) => context
|
||||
.read<CommandPaletteBloc>()
|
||||
.add(CommandPaletteEvent.searchChanged(search: value)),
|
||||
),
|
||||
),
|
||||
if (enableShowAISearch)
|
||||
MobileSearchAskAiEntrance(query: state.query),
|
||||
Flexible(child: SafeArea(child: MobileSearchResult())),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
|
||||
@@ -6,7 +6,6 @@ import 'package:appflowy/workspace/application/command_palette/command_palette_b
|
||||
import 'package:appflowy/workspace/application/command_palette/search_result_list_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/recent/recent_views_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_service.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:appflowy_result/appflowy_result.dart';
|
||||
import 'package:appflowy_ui/appflowy_ui.dart';
|
||||
@@ -37,18 +36,13 @@ class MobileSearchRecentList extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = AppFlowyTheme.of(context);
|
||||
final commandPaletteState = context.read<CommandPaletteBloc>().state;
|
||||
|
||||
final trashIdSet = commandPaletteState.trash.map((e) => e.id).toSet();
|
||||
return BlocProvider(
|
||||
create: (context) =>
|
||||
RecentViewsBloc()..add(const RecentViewsEvent.initial()),
|
||||
child: BlocBuilder<RecentViewsBloc, RecentViewsState>(
|
||||
builder: (context, state) {
|
||||
final List<ViewPB> recentViews = state.views
|
||||
.map((e) => e.item)
|
||||
.where((e) => !trashIdSet.contains(e.id))
|
||||
.toList();
|
||||
final List<ViewPB> recentViews =
|
||||
state.views.map((e) => e.item).toSet().toList();
|
||||
return SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -121,7 +115,7 @@ class _MobileSearchResultListState extends State<MobileSearchResultList> {
|
||||
children: [
|
||||
const VSpace(16),
|
||||
Text(
|
||||
LocaleKeys.commandPalette_bestMatches.tr(),
|
||||
LocaleKeys.search_bestMatch.tr(),
|
||||
style: theme.textStyle.body
|
||||
.enhanced(color: theme.textColorScheme.secondary),
|
||||
),
|
||||
@@ -136,10 +130,6 @@ class _MobileSearchResultListState extends State<MobileSearchResultList> {
|
||||
.fold((s) => s, (s) => null);
|
||||
if (view != null && context.mounted) {
|
||||
await _goToView(context, view);
|
||||
} else {
|
||||
Log.error(
|
||||
'tapping search result, view not found: ${item.id}',
|
||||
);
|
||||
}
|
||||
},
|
||||
child: MobileSearchResultCell(
|
||||
@@ -171,13 +161,12 @@ class _MobileSearchResultListState extends State<MobileSearchResultList> {
|
||||
),
|
||||
const VSpace(12),
|
||||
Text(
|
||||
LocaleKeys.search_noResultForSearching.tr(),
|
||||
LocaleKeys.search_noResultForSearching.tr(args: [query]),
|
||||
style: theme.textStyle.body.enhanced(color: textColor),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
Text(
|
||||
textAlign: TextAlign.center,
|
||||
LocaleKeys.search_noResultForSearchingHint.tr(),
|
||||
style: theme.textStyle.caption.standard(color: textColor),
|
||||
),
|
||||
|
||||
+13
-23
@@ -11,13 +11,11 @@ class MobileSearchTextfield extends StatefulWidget {
|
||||
this.onChanged,
|
||||
required this.hintText,
|
||||
required this.query,
|
||||
required this.focusNode,
|
||||
});
|
||||
|
||||
final String hintText;
|
||||
final String query;
|
||||
final ValueChanged<String>? onChanged;
|
||||
final FocusNode focusNode;
|
||||
|
||||
@override
|
||||
State<MobileSearchTextfield> createState() => _MobileSearchTextfieldState();
|
||||
@@ -25,15 +23,18 @@ class MobileSearchTextfield extends StatefulWidget {
|
||||
|
||||
class _MobileSearchTextfieldState extends State<MobileSearchTextfield> {
|
||||
late final TextEditingController controller;
|
||||
late final FocusNode focusNode;
|
||||
final ValueNotifier<bool> hasFocusValueNotifier = ValueNotifier(true);
|
||||
|
||||
FocusNode get focusNode => widget.focusNode;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
controller = TextEditingController(text: widget.query);
|
||||
focusNode.addListener(onFocusChanged);
|
||||
focusNode = FocusNode();
|
||||
focusNode.addListener(() {
|
||||
if (!mounted) return;
|
||||
hasFocusValueNotifier.value = focusNode.hasFocus;
|
||||
});
|
||||
controller.addListener(() {
|
||||
if (!mounted) return;
|
||||
widget.onChanged?.call(controller.text);
|
||||
@@ -45,7 +46,7 @@ class _MobileSearchTextfieldState extends State<MobileSearchTextfield> {
|
||||
@override
|
||||
void dispose() {
|
||||
controller.dispose();
|
||||
focusNode.removeListener(onFocusChanged);
|
||||
focusNode.dispose();
|
||||
hasFocusValueNotifier.dispose();
|
||||
bottomNavigationBarItemType.removeListener(onBackOrLeave);
|
||||
super.dispose();
|
||||
@@ -55,7 +56,7 @@ class _MobileSearchTextfieldState extends State<MobileSearchTextfield> {
|
||||
Widget build(BuildContext context) {
|
||||
final theme = AppFlowyTheme.of(context);
|
||||
return SizedBox(
|
||||
height: 42,
|
||||
height: 40,
|
||||
child: ValueListenableBuilder(
|
||||
valueListenable: controller,
|
||||
builder: (context, _, __) {
|
||||
@@ -84,17 +85,12 @@ class _MobileSearchTextfieldState extends State<MobileSearchTextfield> {
|
||||
if (!hasFocus || !hasText) return SizedBox.shrink();
|
||||
return GestureDetector(
|
||||
onTap: () => focusNode.unfocus(),
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: Container(
|
||||
height: 42,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(left: 8),
|
||||
child: Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
LocaleKeys.button_cancel.tr(),
|
||||
style: theme.textStyle.body
|
||||
.standard(color: theme.textColorScheme.action),
|
||||
),
|
||||
child: Text(
|
||||
LocaleKeys.button_cancel.tr(),
|
||||
style: theme.textStyle.body
|
||||
.standard(color: theme.textColorScheme.action),
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -161,17 +157,11 @@ class _MobileSearchTextfieldState extends State<MobileSearchTextfield> {
|
||||
);
|
||||
}
|
||||
|
||||
void onFocusChanged() {
|
||||
if (!mounted) return;
|
||||
hasFocusValueNotifier.value = focusNode.hasFocus;
|
||||
}
|
||||
|
||||
void onBackOrLeave() {
|
||||
final label = bottomNavigationBarItemType.value;
|
||||
if (label == BottomNavigationBarItemType.search.label) {
|
||||
focusNode.requestFocus();
|
||||
} else {
|
||||
focusNode.unfocus();
|
||||
controller.clear();
|
||||
}
|
||||
}
|
||||
|
||||
+1
-6
@@ -2,7 +2,6 @@ import 'dart:math';
|
||||
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:appflowy_ui/appflowy_ui.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
@@ -329,7 +328,6 @@ class _MobileSelectionMenuWidgetState extends State<MobileSelectionMenuWidget> {
|
||||
}
|
||||
|
||||
Widget _buildNoResultsWidget(BuildContext context) {
|
||||
final theme = AppFlowyTheme.of(context);
|
||||
return DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).cardColor,
|
||||
@@ -352,10 +350,7 @@ class _MobileSelectionMenuWidgetState extends State<MobileSelectionMenuWidget> {
|
||||
child: Center(
|
||||
child: Text(
|
||||
LocaleKeys.inlineActions_noResults.tr(),
|
||||
style: TextStyle(
|
||||
fontSize: 18.0,
|
||||
color: theme.textColorScheme.primary,
|
||||
),
|
||||
style: TextStyle(fontSize: 18.0, color: Color(0x801F2225)),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
|
||||
+10
-11
@@ -1,9 +1,9 @@
|
||||
import 'package:appflowy/core/helpers/url_launcher.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/mobile/presentation/setting/widgets/mobile_setting_trailing.dart';
|
||||
import 'package:appflowy/startup/tasks/device_info_task.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/feature_flags/mobile_feature_flag_screen.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
@@ -22,23 +22,23 @@ class AboutSettingGroup extends StatelessWidget {
|
||||
settingItemList: [
|
||||
MobileSettingItem(
|
||||
name: LocaleKeys.settings_mobile_privacyPolicy.tr(),
|
||||
trailing: MobileSettingTrailing(
|
||||
text: '',
|
||||
trailing: const Icon(
|
||||
Icons.chevron_right,
|
||||
),
|
||||
onTap: () => afLaunchUrlString('https://appflowy.com/privacy'),
|
||||
),
|
||||
MobileSettingItem(
|
||||
name: LocaleKeys.settings_mobile_termsAndConditions.tr(),
|
||||
trailing: MobileSettingTrailing(
|
||||
text: '',
|
||||
trailing: const Icon(
|
||||
Icons.chevron_right,
|
||||
),
|
||||
onTap: () => afLaunchUrlString('https://appflowy.com/terms'),
|
||||
),
|
||||
if (kDebugMode)
|
||||
MobileSettingItem(
|
||||
name: 'Feature Flags',
|
||||
trailing: MobileSettingTrailing(
|
||||
text: '',
|
||||
trailing: const Icon(
|
||||
Icons.chevron_right,
|
||||
),
|
||||
onTap: () {
|
||||
context.push(FeatureFlagScreen.routeName);
|
||||
@@ -46,10 +46,9 @@ class AboutSettingGroup extends StatelessWidget {
|
||||
),
|
||||
MobileSettingItem(
|
||||
name: LocaleKeys.settings_mobile_version.tr(),
|
||||
trailing: MobileSettingTrailing(
|
||||
text:
|
||||
'${ApplicationInfo.applicationVersion} (${ApplicationInfo.buildNumber})',
|
||||
showArrow: false,
|
||||
trailing: FlowyText(
|
||||
'${ApplicationInfo.applicationVersion} (${ApplicationInfo.buildNumber})',
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
+17
-3
@@ -2,11 +2,11 @@ import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart';
|
||||
import 'package:appflowy/mobile/presentation/setting/widgets/mobile_setting_group_widget.dart';
|
||||
import 'package:appflowy/mobile/presentation/setting/widgets/mobile_setting_item_widget.dart';
|
||||
import 'package:appflowy/mobile/presentation/setting/widgets/mobile_setting_trailing.dart';
|
||||
import 'package:appflowy/mobile/presentation/widgets/flowy_option_tile.dart';
|
||||
import 'package:appflowy/workspace/application/settings/ai/settings_ai_bloc.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
@@ -23,6 +23,7 @@ class AiSettingsGroup extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return BlocProvider(
|
||||
create: (context) => SettingsAIBloc(
|
||||
userProfile,
|
||||
@@ -35,8 +36,21 @@ class AiSettingsGroup extends StatelessWidget {
|
||||
settingItemList: [
|
||||
MobileSettingItem(
|
||||
name: LocaleKeys.settings_aiPage_keys_llmModelType.tr(),
|
||||
trailing: MobileSettingTrailing(
|
||||
text: state.availableModels?.selectedModel.name ?? "",
|
||||
trailing: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 200),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Flexible(
|
||||
child: FlowyText(
|
||||
state.availableModels?.selectedModel.name ?? "",
|
||||
color: theme.colorScheme.onSurface,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
const Icon(Icons.chevron_right),
|
||||
],
|
||||
),
|
||||
),
|
||||
onTap: () => _onLLMModelTypeTap(context, state),
|
||||
),
|
||||
|
||||
+11
-3
@@ -1,10 +1,10 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
|
||||
import 'package:appflowy/mobile/presentation/setting/widgets/mobile_setting_trailing.dart';
|
||||
import 'package:appflowy/mobile/presentation/widgets/widgets.dart';
|
||||
import 'package:appflowy/plugins/document/application/document_appearance_cubit.dart';
|
||||
import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
@@ -17,12 +17,20 @@ class RTLSetting extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final textDirection =
|
||||
context.watch<AppearanceSettingsCubit>().state.textDirection;
|
||||
return MobileSettingItem(
|
||||
name: LocaleKeys.settings_appearance_textDirection_label.tr(),
|
||||
trailing: MobileSettingTrailing(
|
||||
text: _textDirectionLabelText(textDirection),
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
FlowyText(
|
||||
_textDirectionLabelText(textDirection),
|
||||
color: theme.colorScheme.onSurface,
|
||||
),
|
||||
const Icon(Icons.chevron_right),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
showMobileBottomSheet(
|
||||
|
||||
+11
-3
@@ -1,11 +1,11 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
|
||||
import 'package:appflowy/mobile/presentation/setting/widgets/mobile_setting_trailing.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/startup/tasks/app_window_size_manager.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/hotkeys.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/more_view_actions/widgets/font_size_stepper.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:scaled_app/scaled_app.dart';
|
||||
|
||||
@@ -42,10 +42,18 @@ class _DisplaySizeSettingState extends State<DisplaySizeSetting> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return MobileSettingItem(
|
||||
name: LocaleKeys.settings_appearance_displaySize.tr(),
|
||||
trailing: MobileSettingTrailing(
|
||||
text: scaleFactor.toStringAsFixed(1),
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
FlowyText(
|
||||
scaleFactor.toStringAsFixed(2),
|
||||
color: theme.colorScheme.onSurface,
|
||||
),
|
||||
const Icon(Icons.chevron_right),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
showMobileBottomSheet(
|
||||
|
||||
+11
-3
@@ -1,11 +1,11 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
|
||||
import 'package:appflowy/mobile/presentation/setting/widgets/mobile_setting_trailing.dart';
|
||||
import 'package:appflowy/mobile/presentation/widgets/widgets.dart';
|
||||
import 'package:appflowy/util/theme_mode_extension.dart';
|
||||
import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
@@ -18,11 +18,19 @@ class ThemeSetting extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final themeMode = context.watch<AppearanceSettingsCubit>().state.themeMode;
|
||||
return MobileSettingItem(
|
||||
name: LocaleKeys.settings_appearance_themeMode_label.tr(),
|
||||
trailing: MobileSettingTrailing(
|
||||
text: themeMode.labelText,
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
FlowyText(
|
||||
themeMode.labelText,
|
||||
color: theme.colorScheme.onSurface,
|
||||
),
|
||||
const Icon(Icons.chevron_right),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
showMobileBottomSheet(
|
||||
|
||||
+15
-19
@@ -1,11 +1,11 @@
|
||||
import 'package:appflowy/env/cloud_env.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/mobile/presentation/setting/cloud/appflowy_cloud_page.dart';
|
||||
import 'package:appflowy/mobile/presentation/setting/widgets/mobile_setting_group_widget.dart';
|
||||
import 'package:appflowy/mobile/presentation/setting/widgets/mobile_setting_item_widget.dart';
|
||||
import 'package:appflowy/mobile/presentation/setting/widgets/mobile_setting_trailing.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/setting_cloud.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
|
||||
class CloudSettingGroup extends StatelessWidget {
|
||||
const CloudSettingGroup({
|
||||
@@ -15,23 +15,19 @@ class CloudSettingGroup extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FutureBuilder(
|
||||
future: getAuthenticatorType(),
|
||||
builder: (context, snapshot) {
|
||||
final cloudType = snapshot.data ?? AuthenticatorType.appflowyCloud;
|
||||
final name = titleFromCloudType(cloudType);
|
||||
return MobileSettingGroup(
|
||||
groupTitle: 'Cloud settings',
|
||||
settingItemList: [
|
||||
MobileSettingItem(
|
||||
name: 'Cloud server',
|
||||
trailing: MobileSettingTrailing(
|
||||
text: name,
|
||||
),
|
||||
onTap: () => context.push(AppFlowyCloudPage.routeName),
|
||||
future: PackageInfo.fromPlatform(),
|
||||
builder: (context, snapshot) => MobileSettingGroup(
|
||||
groupTitle: LocaleKeys.settings_menu_cloudSettings.tr(),
|
||||
settingItemList: [
|
||||
MobileSettingItem(
|
||||
name: LocaleKeys.settings_menu_cloudAppFlowy.tr(),
|
||||
trailing: const Icon(
|
||||
Icons.chevron_right,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
onTap: () => context.push(AppFlowyCloudPage.routeName),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -2,11 +2,11 @@ import 'dart:async';
|
||||
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/mobile/presentation/setting/font/font_picker_screen.dart';
|
||||
import 'package:appflowy/mobile/presentation/setting/widgets/mobile_setting_trailing.dart';
|
||||
import 'package:appflowy/plugins/document/application/document_appearance_cubit.dart';
|
||||
import 'package:appflowy/util/font_family_extension.dart';
|
||||
import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
@@ -20,12 +20,21 @@ class FontSetting extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final selectedFont = context.watch<AppearanceSettingsCubit>().state.font;
|
||||
final name = selectedFont.fontFamilyDisplayName;
|
||||
return MobileSettingItem(
|
||||
name: LocaleKeys.settings_appearance_fontFamily_label.tr(),
|
||||
trailing: MobileSettingTrailing(
|
||||
text: name,
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
FlowyText(
|
||||
lineHeight: 1.0,
|
||||
name,
|
||||
color: theme.colorScheme.onSurface,
|
||||
),
|
||||
const Icon(Icons.chevron_right),
|
||||
],
|
||||
),
|
||||
onTap: () async {
|
||||
final newFont = await context.push<String>(FontPickerScreen.routeName);
|
||||
|
||||
+12
-3
@@ -1,9 +1,9 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/mobile/presentation/setting/language/language_picker_screen.dart';
|
||||
import 'package:appflowy/mobile/presentation/setting/widgets/mobile_setting_trailing.dart';
|
||||
import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/language.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
@@ -28,13 +28,22 @@ class _LanguageSettingGroupState extends State<LanguageSettingGroup> {
|
||||
return state.locale;
|
||||
},
|
||||
builder: (context, locale) {
|
||||
final theme = Theme.of(context);
|
||||
return MobileSettingGroup(
|
||||
groupTitle: LocaleKeys.settings_menu_language.tr(),
|
||||
settingItemList: [
|
||||
MobileSettingItem(
|
||||
name: LocaleKeys.settings_menu_language.tr(),
|
||||
trailing: MobileSettingTrailing(
|
||||
text: languageFromLocale(locale),
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
FlowyText(
|
||||
lineHeight: 1.0,
|
||||
languageFromLocale(locale),
|
||||
color: theme.colorScheme.onSurface,
|
||||
),
|
||||
const Icon(Icons.chevron_right),
|
||||
],
|
||||
),
|
||||
onTap: () async {
|
||||
final newLocale =
|
||||
|
||||
+6
-6
@@ -1,5 +1,4 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy_ui/appflowy_ui.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -65,11 +64,12 @@ class _EditUsernameBottomSheetState extends State<EditUsernameBottomSheet> {
|
||||
),
|
||||
),
|
||||
const VSpace(16),
|
||||
AFFilledTextButton.primary(
|
||||
text: LocaleKeys.button_update.tr(),
|
||||
onTap: submitUserName,
|
||||
size: AFButtonSize.l,
|
||||
alignment: Alignment.center,
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
onPressed: submitUserName,
|
||||
child: Text(LocaleKeys.button_update.tr()),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
+16
-125
@@ -1,16 +1,10 @@
|
||||
import 'package:appflowy/env/cloud_env.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
|
||||
import 'package:appflowy/mobile/presentation/setting/widgets/mobile_setting_trailing.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/user/application/auth/auth_service.dart';
|
||||
import 'package:appflowy/user/application/password/password_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/user/prelude.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/pages/account/password/change_password.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/pages/account/password/setup_password.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
|
||||
import 'package:appflowy_ui/appflowy_ui.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/widget/spacing.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
@@ -27,19 +21,11 @@ class PersonalInfoSettingGroup extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider<SettingsUserViewBloc>(
|
||||
create: (context) => getIt<SettingsUserViewBloc>(
|
||||
param1: userProfile,
|
||||
)..add(const SettingsUserEvent.initial()),
|
||||
),
|
||||
BlocProvider(
|
||||
create: (context) => PasswordBloc(userProfile)
|
||||
..add(PasswordEvent.init())
|
||||
..add(PasswordEvent.checkHasPassword()),
|
||||
),
|
||||
],
|
||||
final theme = Theme.of(context);
|
||||
return BlocProvider<SettingsUserViewBloc>(
|
||||
create: (context) => getIt<SettingsUserViewBloc>(
|
||||
param1: userProfile,
|
||||
)..add(const SettingsUserEvent.initial()),
|
||||
child: BlocSelector<SettingsUserViewBloc, SettingsUserState, String>(
|
||||
selector: (state) => state.userProfile.name,
|
||||
builder: (context, userName) {
|
||||
@@ -47,10 +33,16 @@ class PersonalInfoSettingGroup extends StatelessWidget {
|
||||
groupTitle: LocaleKeys.settings_accountPage_title.tr(),
|
||||
settingItemList: [
|
||||
MobileSettingItem(
|
||||
name: 'User name',
|
||||
trailing: MobileSettingTrailing(
|
||||
text: userName,
|
||||
),
|
||||
name: userName,
|
||||
subtitle: isAuthEnabled && userProfile.email.isNotEmpty
|
||||
? Text(
|
||||
userProfile.email,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: theme.colorScheme.onSurface,
|
||||
),
|
||||
)
|
||||
: null,
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
onTap: () {
|
||||
showMobileBottomSheet(
|
||||
context,
|
||||
@@ -72,111 +64,10 @@ class PersonalInfoSettingGroup extends StatelessWidget {
|
||||
);
|
||||
},
|
||||
),
|
||||
...userProfile.userAuthType == AuthTypePB.Server
|
||||
? [
|
||||
_buildEmailItem(context, userProfile),
|
||||
_buildPasswordItem(context, userProfile),
|
||||
]
|
||||
: [
|
||||
_buildLoginItem(context, userProfile),
|
||||
],
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEmailItem(BuildContext context, UserProfilePB userProfile) {
|
||||
final theme = AppFlowyTheme.of(context);
|
||||
return MobileSettingItem(
|
||||
name: LocaleKeys.settings_accountPage_email_title.tr(),
|
||||
trailing: Text(
|
||||
userProfile.email,
|
||||
style: theme.textStyle.heading4.standard(
|
||||
color: theme.textColorScheme.secondary,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPasswordItem(BuildContext context, UserProfilePB userProfile) {
|
||||
return BlocBuilder<PasswordBloc, PasswordState>(
|
||||
builder: (context, state) {
|
||||
final hasPassword = state.hasPassword;
|
||||
final title = hasPassword
|
||||
? LocaleKeys.newSettings_myAccount_password_changePassword.tr()
|
||||
: LocaleKeys.newSettings_myAccount_password_setupPassword.tr();
|
||||
final passwordBloc = context.read<PasswordBloc>();
|
||||
return MobileSettingItem(
|
||||
name: LocaleKeys.newSettings_myAccount_password_title.tr(),
|
||||
trailing: MobileSettingTrailing(
|
||||
text: '',
|
||||
),
|
||||
onTap: () {
|
||||
showMobileBottomSheet(
|
||||
context,
|
||||
showHeader: true,
|
||||
title: title,
|
||||
showCloseButton: true,
|
||||
showDragHandle: true,
|
||||
showDivider: false,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
builder: (_) {
|
||||
Widget child;
|
||||
if (hasPassword) {
|
||||
child = ChangePasswordDialogContent(
|
||||
userProfile: userProfile,
|
||||
showTitle: false,
|
||||
showCloseAndSaveButton: false,
|
||||
showSaveButton: true,
|
||||
padding: EdgeInsets.zero,
|
||||
);
|
||||
} else {
|
||||
child = SetupPasswordDialogContent(
|
||||
userProfile: userProfile,
|
||||
showTitle: false,
|
||||
showCloseAndSaveButton: false,
|
||||
showSaveButton: true,
|
||||
padding: EdgeInsets.zero,
|
||||
);
|
||||
}
|
||||
return BlocProvider.value(
|
||||
value: passwordBloc,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLoginItem(BuildContext context, UserProfilePB userProfile) {
|
||||
final theme = AppFlowyTheme.of(context);
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
LocaleKeys.signIn_youAreInLocalMode.tr(),
|
||||
style: theme.textStyle.body.standard(
|
||||
color: theme.textColorScheme.secondary,
|
||||
),
|
||||
),
|
||||
VSpace(theme.spacing.m),
|
||||
AFOutlinedTextButton.normal(
|
||||
text: LocaleKeys.signIn_loginToAppFlowyCloud.tr(),
|
||||
size: AFButtonSize.l,
|
||||
alignment: Alignment.center,
|
||||
onTap: () async {
|
||||
// logout and restart the app
|
||||
await getIt<AuthService>().signOut();
|
||||
await runAppFlowy();
|
||||
},
|
||||
),
|
||||
VSpace(theme.spacing.m),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -87,7 +87,7 @@ class _SelfHostUrlBottomSheetState extends State<SelfHostUrlBottomSheet> {
|
||||
case SelfHostUrlBottomSheetType.shareDomain:
|
||||
await useBaseWebDomain(url);
|
||||
case SelfHostUrlBottomSheetType.cloudURL:
|
||||
await useSelfHostedAppFlowyCloud(url);
|
||||
await useSelfHostedAppFlowyCloudWithURL(url);
|
||||
}
|
||||
await runAppFlowy();
|
||||
},
|
||||
|
||||
+6
-7
@@ -3,7 +3,6 @@ import 'dart:io';
|
||||
import 'package:appflowy/core/helpers/url_launcher.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart';
|
||||
import 'package:appflowy/mobile/presentation/setting/widgets/mobile_setting_trailing.dart';
|
||||
import 'package:appflowy/mobile/presentation/widgets/widgets.dart';
|
||||
import 'package:appflowy/shared/appflowy_cache_manager.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
@@ -31,15 +30,15 @@ class SupportSettingGroup extends StatelessWidget {
|
||||
settingItemList: [
|
||||
MobileSettingItem(
|
||||
name: LocaleKeys.settings_mobile_joinDiscord.tr(),
|
||||
trailing: MobileSettingTrailing(
|
||||
text: '',
|
||||
trailing: const Icon(
|
||||
Icons.chevron_right,
|
||||
),
|
||||
onTap: () => afLaunchUrlString('https://discord.gg/JucBXeU2FE'),
|
||||
),
|
||||
MobileSettingItem(
|
||||
name: LocaleKeys.workspace_errorActions_reportIssue.tr(),
|
||||
trailing: MobileSettingTrailing(
|
||||
text: '',
|
||||
trailing: const Icon(
|
||||
Icons.chevron_right,
|
||||
),
|
||||
onTap: () {
|
||||
showMobileBottomSheet(
|
||||
@@ -58,8 +57,8 @@ class SupportSettingGroup extends StatelessWidget {
|
||||
),
|
||||
MobileSettingItem(
|
||||
name: LocaleKeys.settings_files_clearCache.tr(),
|
||||
trailing: MobileSettingTrailing(
|
||||
text: '',
|
||||
trailing: const Icon(
|
||||
Icons.chevron_right,
|
||||
),
|
||||
onTap: () async {
|
||||
await showFlowyMobileConfirmDialog(
|
||||
|
||||
+6
-14
@@ -9,7 +9,6 @@ import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/widget
|
||||
import 'package:appflowy/workspace/presentation/settings/pages/account/account_deletion.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
|
||||
import 'package:appflowy_ui/appflowy_ui.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -27,12 +26,11 @@ class UserSessionSettingGroup extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = AppFlowyTheme.of(context);
|
||||
return Column(
|
||||
children: [
|
||||
// third party sign in buttons
|
||||
if (showThirdPartyLogin) _buildThirdPartySignInButtons(context),
|
||||
VSpace(theme.spacing.xxl),
|
||||
const VSpace(8.0),
|
||||
|
||||
// logout button
|
||||
MobileLogoutButton(
|
||||
@@ -42,20 +40,14 @@ class UserSessionSettingGroup extends StatelessWidget {
|
||||
|
||||
// delete account button
|
||||
// only show the delete account button in cloud mode
|
||||
if (userProfile.userAuthType == AuthTypePB.Server) ...[
|
||||
VSpace(theme.spacing.xxl),
|
||||
AFOutlinedTextButton.destructive(
|
||||
alignment: Alignment.center,
|
||||
if (userProfile.workspaceAuthType == AuthTypePB.Server) ...[
|
||||
const VSpace(16.0),
|
||||
MobileLogoutButton(
|
||||
text: LocaleKeys.button_deleteAccount.tr(),
|
||||
textStyle: theme.textStyle.body.standard(
|
||||
color: theme.textColorScheme.error,
|
||||
),
|
||||
onTap: () => _showDeleteAccountDialog(context),
|
||||
size: AFButtonSize.l,
|
||||
textColor: Theme.of(context).colorScheme.error,
|
||||
onPressed: () => _showDeleteAccountDialog(context),
|
||||
),
|
||||
],
|
||||
|
||||
VSpace(theme.spacing.xxl),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
+4
-11
@@ -1,4 +1,3 @@
|
||||
import 'package:appflowy_ui/appflowy_ui.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
@@ -16,22 +15,16 @@ class MobileSettingGroup extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = AppFlowyTheme.of(context);
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
VSpace(theme.spacing.s),
|
||||
Text(
|
||||
const VSpace(4.0),
|
||||
FlowyText.semibold(
|
||||
groupTitle,
|
||||
style: theme.textStyle.heading4.enhanced(
|
||||
color: theme.textColorScheme.primary,
|
||||
),
|
||||
),
|
||||
VSpace(theme.spacing.s),
|
||||
const VSpace(4.0),
|
||||
...settingItemList,
|
||||
showDivider
|
||||
? AFDivider(spacing: theme.spacing.m)
|
||||
: const SizedBox.shrink(),
|
||||
showDivider ? const Divider() : const SizedBox.shrink(),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
+5
-9
@@ -1,4 +1,3 @@
|
||||
import 'package:appflowy_ui/appflowy_ui.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
@@ -27,18 +26,17 @@ class MobileSettingItem extends StatelessWidget {
|
||||
return Padding(
|
||||
padding: padding,
|
||||
child: ListTile(
|
||||
title: title ?? _buildDefaultTitle(context, name),
|
||||
title: title ?? _buildDefaultTitle(name),
|
||||
subtitle: subtitle,
|
||||
trailing: trailing,
|
||||
onTap: onTap,
|
||||
visualDensity: VisualDensity.compact,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
contentPadding: const EdgeInsets.only(left: 8.0),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDefaultTitle(BuildContext context, String? name) {
|
||||
final theme = AppFlowyTheme.of(context);
|
||||
Widget _buildDefaultTitle(String? name) {
|
||||
return Row(
|
||||
children: [
|
||||
if (leadingIcon != null) ...[
|
||||
@@ -46,11 +44,9 @@ class MobileSettingItem extends StatelessWidget {
|
||||
const HSpace(8),
|
||||
],
|
||||
Expanded(
|
||||
child: Text(
|
||||
child: FlowyText.medium(
|
||||
name ?? '',
|
||||
style: theme.textStyle.heading4.standard(
|
||||
color: theme.textColorScheme.primary,
|
||||
),
|
||||
fontSize: 14.0,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
|
||||
-42
@@ -1,42 +0,0 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy_ui/appflowy_ui.dart';
|
||||
import 'package:flowy_infra_ui/widget/spacing.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class MobileSettingTrailing extends StatelessWidget {
|
||||
const MobileSettingTrailing({
|
||||
super.key,
|
||||
required this.text,
|
||||
this.showArrow = true,
|
||||
});
|
||||
|
||||
final String text;
|
||||
final bool showArrow;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = AppFlowyTheme.of(context);
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
text,
|
||||
style: theme.textStyle.heading4.standard(
|
||||
color: theme.textColorScheme.secondary,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
if (showArrow) ...[
|
||||
const HSpace(8),
|
||||
FlowySvg(
|
||||
FlowySvgs.toolbar_arrow_right_m,
|
||||
size: Size.square(24),
|
||||
color: theme.iconColorScheme.tertiary,
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
-324
@@ -1,324 +0,0 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/mobile/presentation/base/app_bar/app_bar.dart';
|
||||
import 'package:appflowy/mobile/presentation/widgets/show_flowy_mobile_confirm_dialog.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';
|
||||
import 'package:appflowy/shared/af_role_pb_extension.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/user/application/user_service.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/members/inivitation/m_invite_member_by_email.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/members/workspace_member_bloc.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/code.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
|
||||
import 'package:appflowy_result/appflowy_result.dart';
|
||||
import 'package:appflowy_ui/appflowy_ui.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import 'member_list.dart';
|
||||
|
||||
class AddMembersScreen extends StatelessWidget {
|
||||
const AddMembersScreen({
|
||||
super.key,
|
||||
});
|
||||
|
||||
static const routeName = '/add_member';
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: FlowyAppBar(
|
||||
titleText: 'Add members',
|
||||
),
|
||||
body: const _InviteMemberPage(),
|
||||
resizeToAvoidBottomInset: false,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _InviteMemberPage extends StatefulWidget {
|
||||
const _InviteMemberPage();
|
||||
|
||||
@override
|
||||
State<_InviteMemberPage> createState() => _InviteMemberPageState();
|
||||
}
|
||||
|
||||
class _InviteMemberPageState extends State<_InviteMemberPage> {
|
||||
final emailController = TextEditingController();
|
||||
late final Future<UserProfilePB?> userProfile;
|
||||
bool exceededLimit = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
userProfile = UserBackendService.getCurrentUserProfile().fold(
|
||||
(s) => s,
|
||||
(f) => null,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
emailController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = AppFlowyTheme.of(context);
|
||||
return FutureBuilder(
|
||||
future: userProfile,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
if (snapshot.hasError || snapshot.data == null) {
|
||||
return _buildError(context);
|
||||
}
|
||||
|
||||
final userProfile = snapshot.data!;
|
||||
|
||||
return BlocProvider<WorkspaceMemberBloc>(
|
||||
create: (context) => WorkspaceMemberBloc(userProfile: userProfile)
|
||||
..add(const WorkspaceMemberEvent.initial())
|
||||
..add(const WorkspaceMemberEvent.getInviteCode()),
|
||||
child: BlocConsumer<WorkspaceMemberBloc, WorkspaceMemberState>(
|
||||
listener: _onListener,
|
||||
builder: (context, state) {
|
||||
return Column(
|
||||
children: [
|
||||
if (state.myRole.isOwner) ...[
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: EdgeInsets.all(theme.spacing.xl),
|
||||
child: const MInviteMemberByEmail(),
|
||||
),
|
||||
VSpace(theme.spacing.m),
|
||||
],
|
||||
if (state.members.isNotEmpty) ...[
|
||||
const AFDivider(),
|
||||
VSpace(theme.spacing.xl),
|
||||
MobileMemberList(
|
||||
members: state.members,
|
||||
userProfile: userProfile,
|
||||
myRole: state.myRole,
|
||||
),
|
||||
],
|
||||
if (state.myRole.isMember) ...[
|
||||
Spacer(),
|
||||
const _LeaveWorkspaceButton(),
|
||||
],
|
||||
const VSpace(48),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildError(BuildContext context) {
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 48.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
FlowyText.medium(
|
||||
LocaleKeys.settings_appearance_members_workspaceMembersError.tr(),
|
||||
fontSize: 18.0,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const VSpace(8.0),
|
||||
FlowyText.regular(
|
||||
LocaleKeys
|
||||
.settings_appearance_members_workspaceMembersErrorDescription
|
||||
.tr(),
|
||||
fontSize: 17.0,
|
||||
maxLines: 10,
|
||||
textAlign: TextAlign.center,
|
||||
lineHeight: 1.3,
|
||||
color: Theme.of(context).hintColor,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _onListener(BuildContext context, WorkspaceMemberState state) {
|
||||
final actionResult = state.actionResult;
|
||||
if (actionResult == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final actionType = actionResult.actionType;
|
||||
final result = actionResult.result;
|
||||
|
||||
// only show the result dialog when the action is WorkspaceMemberActionType.add
|
||||
if (actionType == WorkspaceMemberActionType.addByEmail) {
|
||||
result.fold(
|
||||
(s) {
|
||||
showToastNotification(
|
||||
message:
|
||||
LocaleKeys.settings_appearance_members_addMemberSuccess.tr(),
|
||||
);
|
||||
},
|
||||
(f) {
|
||||
Log.error('add workspace member failed: $f');
|
||||
final message = f.code == ErrorCode.WorkspaceMemberLimitExceeded
|
||||
? LocaleKeys
|
||||
.settings_appearance_members_inviteFailedMemberLimitMobile
|
||||
.tr()
|
||||
: LocaleKeys.settings_appearance_members_failedToAddMember.tr();
|
||||
setState(() {
|
||||
exceededLimit = f.code == ErrorCode.WorkspaceMemberLimitExceeded;
|
||||
});
|
||||
showToastNotification(
|
||||
type: ToastificationType.error,
|
||||
message: message,
|
||||
);
|
||||
},
|
||||
);
|
||||
} else if (actionType == WorkspaceMemberActionType.inviteByEmail) {
|
||||
result.fold(
|
||||
(s) {
|
||||
showToastNotification(
|
||||
message:
|
||||
LocaleKeys.settings_appearance_members_inviteMemberSuccess.tr(),
|
||||
);
|
||||
},
|
||||
(f) {
|
||||
Log.error('invite workspace member failed: $f');
|
||||
final message = f.code == ErrorCode.WorkspaceMemberLimitExceeded
|
||||
? LocaleKeys
|
||||
.settings_appearance_members_inviteFailedMemberLimitMobile
|
||||
.tr()
|
||||
: LocaleKeys.settings_appearance_members_failedToInviteMember
|
||||
.tr();
|
||||
setState(() {
|
||||
exceededLimit = f.code == ErrorCode.WorkspaceMemberLimitExceeded;
|
||||
});
|
||||
showToastNotification(
|
||||
type: ToastificationType.error,
|
||||
message: message,
|
||||
);
|
||||
},
|
||||
);
|
||||
} else if (actionType == WorkspaceMemberActionType.removeByEmail) {
|
||||
result.fold(
|
||||
(s) {
|
||||
showToastNotification(
|
||||
message: LocaleKeys
|
||||
.settings_appearance_members_removeFromWorkspaceSuccess
|
||||
.tr(),
|
||||
);
|
||||
},
|
||||
(f) {
|
||||
showToastNotification(
|
||||
type: ToastificationType.error,
|
||||
message: LocaleKeys
|
||||
.settings_appearance_members_removeFromWorkspaceFailed
|
||||
.tr(),
|
||||
);
|
||||
},
|
||||
);
|
||||
} else if (actionType == WorkspaceMemberActionType.generateInviteLink) {
|
||||
result.fold(
|
||||
(s) async {
|
||||
showToastNotification(
|
||||
message: LocaleKeys
|
||||
.settings_appearance_members_generatedLinkSuccessfully
|
||||
.tr(),
|
||||
);
|
||||
|
||||
// copy the invite link to the clipboard
|
||||
final inviteLink = state.inviteLink;
|
||||
if (inviteLink != null) {
|
||||
await getIt<ClipboardService>().setPlainText(inviteLink);
|
||||
showToastNotification(
|
||||
message: LocaleKeys.shareAction_copyLinkSuccess.tr(),
|
||||
);
|
||||
}
|
||||
},
|
||||
(f) {
|
||||
Log.error('generate invite link failed: $f');
|
||||
showToastNotification(
|
||||
type: ToastificationType.error,
|
||||
message:
|
||||
LocaleKeys.settings_appearance_members_generatedLinkFailed.tr(),
|
||||
);
|
||||
},
|
||||
);
|
||||
} else if (actionType == WorkspaceMemberActionType.resetInviteLink) {
|
||||
result.fold(
|
||||
(s) async {
|
||||
showToastNotification(
|
||||
message: LocaleKeys
|
||||
.settings_appearance_members_resetLinkSuccessfully
|
||||
.tr(),
|
||||
);
|
||||
|
||||
// copy the invite link to the clipboard
|
||||
final inviteLink = state.inviteLink;
|
||||
if (inviteLink != null) {
|
||||
await getIt<ClipboardService>().setPlainText(inviteLink);
|
||||
showToastNotification(
|
||||
message: LocaleKeys.shareAction_copyLinkSuccess.tr(),
|
||||
);
|
||||
}
|
||||
},
|
||||
(f) {
|
||||
Log.error('generate invite link failed: $f');
|
||||
showToastNotification(
|
||||
type: ToastificationType.error,
|
||||
message:
|
||||
LocaleKeys.settings_appearance_members_resetLinkFailed.tr(),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _LeaveWorkspaceButton extends StatelessWidget {
|
||||
const _LeaveWorkspaceButton();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: AFOutlinedTextButton.destructive(
|
||||
alignment: Alignment.center,
|
||||
text: LocaleKeys.workspace_leaveCurrentWorkspace.tr(),
|
||||
onTap: () => _leaveWorkspace(context),
|
||||
size: AFButtonSize.l,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _leaveWorkspace(BuildContext context) {
|
||||
showFlowyCupertinoConfirmDialog(
|
||||
title: LocaleKeys.workspace_leaveCurrentWorkspacePrompt.tr(),
|
||||
leftButton: FlowyText(
|
||||
LocaleKeys.button_cancel.tr(),
|
||||
fontSize: 17.0,
|
||||
figmaLineHeight: 24.0,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: const Color(0xFF007AFF),
|
||||
),
|
||||
rightButton: FlowyText(
|
||||
LocaleKeys.button_confirm.tr(),
|
||||
fontSize: 17.0,
|
||||
figmaLineHeight: 24.0,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: const Color(0xFFFE0220),
|
||||
),
|
||||
onRightButtonPressed: (buttonContext) async {},
|
||||
);
|
||||
}
|
||||
}
|
||||
-357
@@ -1,357 +0,0 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/mobile/presentation/base/app_bar/app_bar.dart';
|
||||
import 'package:appflowy/mobile/presentation/widgets/show_flowy_mobile_confirm_dialog.dart';
|
||||
import 'package:appflowy/shared/af_role_pb_extension.dart';
|
||||
import 'package:appflowy/user/application/user_service.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/members/workspace_member_bloc.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/code.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
|
||||
import 'package:appflowy_result/appflowy_result.dart';
|
||||
import 'package:appflowy_ui/appflowy_ui.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:string_validator/string_validator.dart';
|
||||
|
||||
import 'member_list.dart';
|
||||
|
||||
ValueNotifier<int> mobileLeaveWorkspaceNotifier = ValueNotifier(0);
|
||||
|
||||
class InviteMembersScreen extends StatelessWidget {
|
||||
const InviteMembersScreen({
|
||||
super.key,
|
||||
});
|
||||
|
||||
static const routeName = '/invite_member';
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: FlowyAppBar(
|
||||
titleText: LocaleKeys.settings_appearance_members_label.tr(),
|
||||
),
|
||||
body: const _InviteMemberPage(),
|
||||
resizeToAvoidBottomInset: false,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _InviteMemberPage extends StatefulWidget {
|
||||
const _InviteMemberPage();
|
||||
|
||||
@override
|
||||
State<_InviteMemberPage> createState() => _InviteMemberPageState();
|
||||
}
|
||||
|
||||
class _InviteMemberPageState extends State<_InviteMemberPage> {
|
||||
final emailController = TextEditingController();
|
||||
late final Future<UserProfilePB?> userProfile;
|
||||
bool exceededLimit = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
userProfile = UserBackendService.getCurrentUserProfile().fold(
|
||||
(s) => s,
|
||||
(f) => null,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
emailController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = AppFlowyTheme.of(context);
|
||||
return FutureBuilder(
|
||||
future: userProfile,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
if (snapshot.hasError || snapshot.data == null) {
|
||||
return _buildError(context);
|
||||
}
|
||||
|
||||
final userProfile = snapshot.data!;
|
||||
|
||||
return BlocProvider<WorkspaceMemberBloc>(
|
||||
create: (context) => WorkspaceMemberBloc(userProfile: userProfile)
|
||||
..add(const WorkspaceMemberEvent.initial())
|
||||
..add(const WorkspaceMemberEvent.getInviteCode()),
|
||||
child: BlocConsumer<WorkspaceMemberBloc, WorkspaceMemberState>(
|
||||
listener: _onListener,
|
||||
builder: (context, state) {
|
||||
return Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (state.myRole.isOwner) ...[
|
||||
Padding(
|
||||
padding: EdgeInsets.all(theme.spacing.xl),
|
||||
child: _buildInviteMemberArea(context),
|
||||
),
|
||||
const VSpace(16),
|
||||
],
|
||||
if (state.members.isNotEmpty) ...[
|
||||
const AFDivider(),
|
||||
VSpace(theme.spacing.xl),
|
||||
MobileMemberList(
|
||||
members: state.members,
|
||||
userProfile: userProfile,
|
||||
myRole: state.myRole,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
if (state.myRole.isMember) const _LeaveWorkspaceButton(),
|
||||
const VSpace(48),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildInviteMemberArea(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
TextFormField(
|
||||
autofocus: true,
|
||||
controller: emailController,
|
||||
keyboardType: TextInputType.text,
|
||||
decoration: InputDecoration(
|
||||
hintText: LocaleKeys.settings_appearance_members_inviteHint.tr(),
|
||||
),
|
||||
),
|
||||
const VSpace(16),
|
||||
if (exceededLimit) ...[
|
||||
FlowyText.regular(
|
||||
LocaleKeys.settings_appearance_members_inviteFailedMemberLimitMobile
|
||||
.tr(),
|
||||
fontSize: 14.0,
|
||||
maxLines: 3,
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
const VSpace(16),
|
||||
],
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
onPressed: () => _inviteMember(context),
|
||||
child: Text(
|
||||
LocaleKeys.settings_appearance_members_sendInvite.tr(),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildError(BuildContext context) {
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 48.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
FlowyText.medium(
|
||||
LocaleKeys.settings_appearance_members_workspaceMembersError.tr(),
|
||||
fontSize: 18.0,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const VSpace(8.0),
|
||||
FlowyText.regular(
|
||||
LocaleKeys
|
||||
.settings_appearance_members_workspaceMembersErrorDescription
|
||||
.tr(),
|
||||
fontSize: 17.0,
|
||||
maxLines: 10,
|
||||
textAlign: TextAlign.center,
|
||||
lineHeight: 1.3,
|
||||
color: Theme.of(context).hintColor,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _onListener(BuildContext context, WorkspaceMemberState state) {
|
||||
final actionResult = state.actionResult;
|
||||
if (actionResult == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final actionType = actionResult.actionType;
|
||||
final result = actionResult.result;
|
||||
|
||||
// get keyboard height
|
||||
final keyboardHeight = MediaQuery.of(context).viewInsets.bottom;
|
||||
|
||||
// only show the result dialog when the action is WorkspaceMemberActionType.add
|
||||
if (actionType == WorkspaceMemberActionType.addByEmail) {
|
||||
result.fold(
|
||||
(s) {
|
||||
showToastNotification(
|
||||
message:
|
||||
LocaleKeys.settings_appearance_members_addMemberSuccess.tr(),
|
||||
bottomPadding: keyboardHeight,
|
||||
);
|
||||
},
|
||||
(f) {
|
||||
Log.error('add workspace member failed: $f');
|
||||
final message = f.code == ErrorCode.WorkspaceMemberLimitExceeded
|
||||
? LocaleKeys
|
||||
.settings_appearance_members_inviteFailedMemberLimitMobile
|
||||
.tr()
|
||||
: LocaleKeys.settings_appearance_members_failedToAddMember.tr();
|
||||
setState(() {
|
||||
exceededLimit = f.code == ErrorCode.WorkspaceMemberLimitExceeded;
|
||||
});
|
||||
showToastNotification(
|
||||
type: ToastificationType.error,
|
||||
bottomPadding: keyboardHeight,
|
||||
message: message,
|
||||
);
|
||||
},
|
||||
);
|
||||
} else if (actionType == WorkspaceMemberActionType.inviteByEmail) {
|
||||
result.fold(
|
||||
(s) {
|
||||
showToastNotification(
|
||||
message:
|
||||
LocaleKeys.settings_appearance_members_inviteMemberSuccess.tr(),
|
||||
bottomPadding: keyboardHeight,
|
||||
);
|
||||
},
|
||||
(f) {
|
||||
Log.error('invite workspace member failed: $f');
|
||||
final message = f.code == ErrorCode.WorkspaceMemberLimitExceeded
|
||||
? LocaleKeys
|
||||
.settings_appearance_members_inviteFailedMemberLimitMobile
|
||||
.tr()
|
||||
: LocaleKeys.settings_appearance_members_failedToInviteMember
|
||||
.tr();
|
||||
setState(() {
|
||||
exceededLimit = f.code == ErrorCode.WorkspaceMemberLimitExceeded;
|
||||
});
|
||||
showToastNotification(
|
||||
type: ToastificationType.error,
|
||||
message: message,
|
||||
bottomPadding: keyboardHeight,
|
||||
);
|
||||
},
|
||||
);
|
||||
} else if (actionType == WorkspaceMemberActionType.removeByEmail) {
|
||||
result.fold(
|
||||
(s) {
|
||||
showToastNotification(
|
||||
message: LocaleKeys
|
||||
.settings_appearance_members_removeFromWorkspaceSuccess
|
||||
.tr(),
|
||||
bottomPadding: keyboardHeight,
|
||||
);
|
||||
},
|
||||
(f) {
|
||||
showToastNotification(
|
||||
type: ToastificationType.error,
|
||||
message: LocaleKeys
|
||||
.settings_appearance_members_removeFromWorkspaceFailed
|
||||
.tr(),
|
||||
bottomPadding: keyboardHeight,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _inviteMember(BuildContext context) {
|
||||
final email = emailController.text;
|
||||
if (!isEmail(email)) {
|
||||
showToastNotification(
|
||||
type: ToastificationType.error,
|
||||
message: LocaleKeys.settings_appearance_members_emailInvalidError.tr(),
|
||||
);
|
||||
return;
|
||||
}
|
||||
context
|
||||
.read<WorkspaceMemberBloc>()
|
||||
.add(WorkspaceMemberEvent.inviteWorkspaceMemberByEmail(email));
|
||||
// clear the email field after inviting
|
||||
emailController.clear();
|
||||
}
|
||||
}
|
||||
|
||||
class _LeaveWorkspaceButton extends StatelessWidget {
|
||||
const _LeaveWorkspaceButton();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.transparent,
|
||||
foregroundColor: Theme.of(context).colorScheme.error,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
side: BorderSide(
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
width: 0.5,
|
||||
),
|
||||
),
|
||||
),
|
||||
onPressed: () => _leaveWorkspace(context),
|
||||
child: FlowyText(
|
||||
LocaleKeys.workspace_leaveCurrentWorkspace.tr(),
|
||||
fontSize: 14.0,
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _leaveWorkspace(BuildContext context) {
|
||||
showFlowyCupertinoConfirmDialog(
|
||||
title: LocaleKeys.workspace_leaveCurrentWorkspacePrompt.tr(),
|
||||
leftButton: FlowyText(
|
||||
LocaleKeys.button_cancel.tr(),
|
||||
fontSize: 17.0,
|
||||
figmaLineHeight: 24.0,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: const Color(0xFF007AFF),
|
||||
),
|
||||
rightButton: FlowyText(
|
||||
LocaleKeys.button_confirm.tr(),
|
||||
fontSize: 17.0,
|
||||
figmaLineHeight: 24.0,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: const Color(0xFFFE0220),
|
||||
),
|
||||
onRightButtonPressed: (buttonContext) async {
|
||||
// try to use popUntil with a specific route name but failed
|
||||
// so use pop twice as a workaround
|
||||
Navigator.of(buttonContext).pop();
|
||||
Navigator.of(context).pop();
|
||||
Navigator.of(context).pop();
|
||||
|
||||
mobileLeaveWorkspaceNotifier.value =
|
||||
mobileLeaveWorkspaceNotifier.value + 1;
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
+111
-114
@@ -1,25 +1,19 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/mobile/presentation/base/app_bar/app_bar.dart';
|
||||
import 'package:appflowy/mobile/presentation/setting/workspace/add_members_screen.dart';
|
||||
import 'package:appflowy/mobile/presentation/widgets/show_flowy_mobile_confirm_dialog.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';
|
||||
import 'package:appflowy/shared/af_role_pb_extension.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/user/application/user_service.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/members/inivitation/m_invite_member_by_link.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/members/workspace_member_bloc.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/code.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
|
||||
import 'package:appflowy_result/appflowy_result.dart';
|
||||
import 'package:appflowy_ui/appflowy_ui.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:string_validator/string_validator.dart';
|
||||
|
||||
import 'member_list.dart';
|
||||
|
||||
@@ -37,26 +31,11 @@ class InviteMembersScreen extends StatelessWidget {
|
||||
return Scaffold(
|
||||
appBar: FlowyAppBar(
|
||||
titleText: LocaleKeys.settings_appearance_members_label.tr(),
|
||||
actions: [
|
||||
_buildAddMemberButton(context),
|
||||
],
|
||||
),
|
||||
body: const _InviteMemberPage(),
|
||||
resizeToAvoidBottomInset: false,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAddMemberButton(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(right: 20),
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
context.push(AddMembersScreen.routeName);
|
||||
},
|
||||
child: FlowySvg(FlowySvgs.add_thin_s),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _InviteMemberPage extends StatefulWidget {
|
||||
@@ -88,7 +67,6 @@ class _InviteMemberPageState extends State<_InviteMemberPage> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = AppFlowyTheme.of(context);
|
||||
return FutureBuilder(
|
||||
future: userProfile,
|
||||
builder: (context, snapshot) {
|
||||
@@ -103,38 +81,37 @@ class _InviteMemberPageState extends State<_InviteMemberPage> {
|
||||
|
||||
return BlocProvider<WorkspaceMemberBloc>(
|
||||
create: (context) => WorkspaceMemberBloc(userProfile: userProfile)
|
||||
..add(const WorkspaceMemberEvent.initial())
|
||||
..add(const WorkspaceMemberEvent.getInviteCode()),
|
||||
..add(const WorkspaceMemberEvent.initial()),
|
||||
child: BlocConsumer<WorkspaceMemberBloc, WorkspaceMemberState>(
|
||||
listener: _onListener,
|
||||
builder: (context, state) {
|
||||
return SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
if (state.myRole.isOwner) ...[
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: EdgeInsets.all(theme.spacing.xl),
|
||||
child: const MInviteMemberByLink(),
|
||||
),
|
||||
VSpace(theme.spacing.m),
|
||||
],
|
||||
if (state.members.isNotEmpty) ...[
|
||||
const AFDivider(),
|
||||
VSpace(theme.spacing.xl),
|
||||
MobileMemberList(
|
||||
members: state.members,
|
||||
userProfile: userProfile,
|
||||
myRole: state.myRole,
|
||||
),
|
||||
],
|
||||
if (state.myRole.isMember) ...[
|
||||
Spacer(),
|
||||
const _LeaveWorkspaceButton(),
|
||||
],
|
||||
const VSpace(48),
|
||||
],
|
||||
),
|
||||
return Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (state.myRole.isOwner) ...[
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: _buildInviteMemberArea(context),
|
||||
),
|
||||
const VSpace(16),
|
||||
],
|
||||
if (state.members.isNotEmpty) ...[
|
||||
const VSpace(8),
|
||||
MobileMemberList(
|
||||
members: state.members,
|
||||
userProfile: userProfile,
|
||||
myRole: state.myRole,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
if (state.myRole.isMember) const _LeaveWorkspaceButton(),
|
||||
const VSpace(48),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
@@ -143,6 +120,41 @@ class _InviteMemberPageState extends State<_InviteMemberPage> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildInviteMemberArea(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
TextFormField(
|
||||
autofocus: true,
|
||||
controller: emailController,
|
||||
keyboardType: TextInputType.text,
|
||||
decoration: InputDecoration(
|
||||
hintText: LocaleKeys.settings_appearance_members_inviteHint.tr(),
|
||||
),
|
||||
),
|
||||
const VSpace(16),
|
||||
if (exceededLimit) ...[
|
||||
FlowyText.regular(
|
||||
LocaleKeys.settings_appearance_members_inviteFailedMemberLimitMobile
|
||||
.tr(),
|
||||
fontSize: 14.0,
|
||||
maxLines: 3,
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
const VSpace(16),
|
||||
],
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
onPressed: () => _inviteMember(context),
|
||||
child: Text(
|
||||
LocaleKeys.settings_appearance_members_sendInvite.tr(),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildError(BuildContext context) {
|
||||
return Center(
|
||||
child: Padding(
|
||||
@@ -181,6 +193,9 @@ class _InviteMemberPageState extends State<_InviteMemberPage> {
|
||||
final actionType = actionResult.actionType;
|
||||
final result = actionResult.result;
|
||||
|
||||
// get keyboard height
|
||||
final keyboardHeight = MediaQuery.of(context).viewInsets.bottom;
|
||||
|
||||
// only show the result dialog when the action is WorkspaceMemberActionType.add
|
||||
if (actionType == WorkspaceMemberActionType.addByEmail) {
|
||||
result.fold(
|
||||
@@ -188,6 +203,7 @@ class _InviteMemberPageState extends State<_InviteMemberPage> {
|
||||
showToastNotification(
|
||||
message:
|
||||
LocaleKeys.settings_appearance_members_addMemberSuccess.tr(),
|
||||
bottomPadding: keyboardHeight,
|
||||
);
|
||||
},
|
||||
(f) {
|
||||
@@ -202,6 +218,7 @@ class _InviteMemberPageState extends State<_InviteMemberPage> {
|
||||
});
|
||||
showToastNotification(
|
||||
type: ToastificationType.error,
|
||||
bottomPadding: keyboardHeight,
|
||||
message: message,
|
||||
);
|
||||
},
|
||||
@@ -212,6 +229,7 @@ class _InviteMemberPageState extends State<_InviteMemberPage> {
|
||||
showToastNotification(
|
||||
message:
|
||||
LocaleKeys.settings_appearance_members_inviteMemberSuccess.tr(),
|
||||
bottomPadding: keyboardHeight,
|
||||
);
|
||||
},
|
||||
(f) {
|
||||
@@ -225,10 +243,10 @@ class _InviteMemberPageState extends State<_InviteMemberPage> {
|
||||
setState(() {
|
||||
exceededLimit = f.code == ErrorCode.WorkspaceMemberLimitExceeded;
|
||||
});
|
||||
|
||||
showToastNotification(
|
||||
type: ToastificationType.error,
|
||||
message: message,
|
||||
bottomPadding: keyboardHeight,
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -239,6 +257,7 @@ class _InviteMemberPageState extends State<_InviteMemberPage> {
|
||||
message: LocaleKeys
|
||||
.settings_appearance_members_removeFromWorkspaceSuccess
|
||||
.tr(),
|
||||
bottomPadding: keyboardHeight,
|
||||
);
|
||||
},
|
||||
(f) {
|
||||
@@ -247,65 +266,28 @@ class _InviteMemberPageState extends State<_InviteMemberPage> {
|
||||
message: LocaleKeys
|
||||
.settings_appearance_members_removeFromWorkspaceFailed
|
||||
.tr(),
|
||||
);
|
||||
},
|
||||
);
|
||||
} else if (actionType == WorkspaceMemberActionType.generateInviteLink) {
|
||||
result.fold(
|
||||
(s) {
|
||||
showToastNotification(
|
||||
message: LocaleKeys
|
||||
.settings_appearance_members_generatedLinkSuccessfully
|
||||
.tr(),
|
||||
);
|
||||
|
||||
// copy the invite link to the clipboard
|
||||
final inviteLink = state.inviteLink;
|
||||
if (inviteLink != null) {
|
||||
getIt<ClipboardService>().setPlainText(inviteLink);
|
||||
showToastNotification(
|
||||
message: LocaleKeys.shareAction_copyLinkSuccess.tr(),
|
||||
);
|
||||
}
|
||||
},
|
||||
(f) {
|
||||
Log.error('generate invite link failed: $f');
|
||||
showToastNotification(
|
||||
type: ToastificationType.error,
|
||||
message:
|
||||
LocaleKeys.settings_appearance_members_generatedLinkFailed.tr(),
|
||||
);
|
||||
},
|
||||
);
|
||||
} else if (actionType == WorkspaceMemberActionType.resetInviteLink) {
|
||||
result.fold(
|
||||
(s) {
|
||||
showToastNotification(
|
||||
message: LocaleKeys
|
||||
.settings_appearance_members_resetLinkSuccessfully
|
||||
.tr(),
|
||||
);
|
||||
|
||||
// copy the invite link to the clipboard
|
||||
final inviteLink = state.inviteLink;
|
||||
if (inviteLink != null) {
|
||||
getIt<ClipboardService>().setPlainText(inviteLink);
|
||||
showToastNotification(
|
||||
message: LocaleKeys.shareAction_copyLinkSuccess.tr(),
|
||||
);
|
||||
}
|
||||
},
|
||||
(f) {
|
||||
Log.error('generate invite link failed: $f');
|
||||
showToastNotification(
|
||||
type: ToastificationType.error,
|
||||
message:
|
||||
LocaleKeys.settings_appearance_members_resetLinkFailed.tr(),
|
||||
bottomPadding: keyboardHeight,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _inviteMember(BuildContext context) {
|
||||
final email = emailController.text;
|
||||
if (!isEmail(email)) {
|
||||
showToastNotification(
|
||||
type: ToastificationType.error,
|
||||
message: LocaleKeys.settings_appearance_members_emailInvalidError.tr(),
|
||||
);
|
||||
return;
|
||||
}
|
||||
context
|
||||
.read<WorkspaceMemberBloc>()
|
||||
.add(WorkspaceMemberEvent.inviteWorkspaceMemberByEmail(email));
|
||||
// clear the email field after inviting
|
||||
emailController.clear();
|
||||
}
|
||||
}
|
||||
|
||||
class _LeaveWorkspaceButton extends StatelessWidget {
|
||||
@@ -313,13 +295,28 @@ class _LeaveWorkspaceButton extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: AFOutlinedTextButton.destructive(
|
||||
alignment: Alignment.center,
|
||||
text: LocaleKeys.workspace_leaveCurrentWorkspace.tr(),
|
||||
onTap: () => _leaveWorkspace(context),
|
||||
size: AFButtonSize.l,
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.transparent,
|
||||
foregroundColor: Theme.of(context).colorScheme.error,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
side: BorderSide(
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
width: 0.5,
|
||||
),
|
||||
),
|
||||
),
|
||||
onPressed: () => _leaveWorkspace(context),
|
||||
child: FlowyText(
|
||||
LocaleKeys.workspace_leaveCurrentWorkspace.tr(),
|
||||
fontSize: 14.0,
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
+40
-49
@@ -5,7 +5,6 @@ import 'package:appflowy/mobile/presentation/widgets/widgets.dart';
|
||||
import 'package:appflowy/shared/af_role_pb_extension.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/members/workspace_member_bloc.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
|
||||
import 'package:appflowy_ui/appflowy_ui.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -28,34 +27,31 @@ class MobileMemberList extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = AppFlowyTheme.of(context);
|
||||
return SingleChildScrollView(
|
||||
child: SlidableAutoCloseBehavior(
|
||||
child: SeparatedColumn(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
separatorBuilder: () => SizedBox.shrink(),
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16.0,
|
||||
vertical: 8.0,
|
||||
),
|
||||
child: Text(
|
||||
'Joined',
|
||||
style: theme.textStyle.heading4.enhanced(
|
||||
color: theme.textColorScheme.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
...members.map(
|
||||
(member) => _MemberItem(
|
||||
member: member,
|
||||
myRole: myRole,
|
||||
userProfile: userProfile,
|
||||
),
|
||||
),
|
||||
],
|
||||
return SlidableAutoCloseBehavior(
|
||||
child: SeparatedColumn(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
separatorBuilder: () => const FlowyDivider(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16.0),
|
||||
),
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16.0,
|
||||
vertical: 8.0,
|
||||
),
|
||||
child: FlowyText.semibold(
|
||||
LocaleKeys.settings_appearance_members_label.tr(),
|
||||
fontSize: 16.0,
|
||||
),
|
||||
),
|
||||
...members.map(
|
||||
(member) => _MemberItem(
|
||||
member: member,
|
||||
myRole: myRole,
|
||||
userProfile: userProfile,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -74,8 +70,8 @@ class _MemberItem extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = AppFlowyTheme.of(context);
|
||||
final canDelete = myRole.canDelete && member.email != userProfile.email;
|
||||
final textColor = member.role.isOwner ? Theme.of(context).hintColor : null;
|
||||
|
||||
Widget child;
|
||||
|
||||
@@ -83,19 +79,17 @@ class _MemberItem extends StatelessWidget {
|
||||
child = Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
child: FlowyText.medium(
|
||||
member.name,
|
||||
style: theme.textStyle.heading4.standard(
|
||||
color: theme.textColorScheme.primary,
|
||||
),
|
||||
color: textColor,
|
||||
fontSize: 15.0,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
child: FlowyText.medium(
|
||||
member.role.description,
|
||||
style: theme.textStyle.heading4.standard(
|
||||
color: theme.textColorScheme.secondary,
|
||||
),
|
||||
color: textColor,
|
||||
fontSize: 15.0,
|
||||
textAlign: TextAlign.end,
|
||||
),
|
||||
),
|
||||
@@ -105,19 +99,18 @@ class _MemberItem extends StatelessWidget {
|
||||
child = Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
child: FlowyText.medium(
|
||||
member.name,
|
||||
style: theme.textStyle.heading4.standard(
|
||||
color: theme.textColorScheme.primary,
|
||||
),
|
||||
color: textColor,
|
||||
fontSize: 15.0,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
const HSpace(36.0),
|
||||
FlowyText.medium(
|
||||
member.role.description,
|
||||
style: theme.textStyle.heading4.standard(
|
||||
color: theme.textColorScheme.secondary,
|
||||
),
|
||||
color: textColor,
|
||||
fontSize: 15.0,
|
||||
textAlign: TextAlign.end,
|
||||
),
|
||||
],
|
||||
@@ -125,10 +118,8 @@ class _MemberItem extends StatelessWidget {
|
||||
}
|
||||
|
||||
child = Container(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: theme.spacing.xl,
|
||||
vertical: theme.spacing.l,
|
||||
),
|
||||
height: 48,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: child,
|
||||
);
|
||||
|
||||
|
||||
+11
-30
@@ -1,10 +1,6 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/mobile/presentation/setting/widgets/mobile_setting_trailing.dart';
|
||||
import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import '../widgets/widgets.dart';
|
||||
@@ -17,32 +13,17 @@ class WorkspaceSettingGroup extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<UserWorkspaceBloc, UserWorkspaceState>(
|
||||
builder: (context, state) {
|
||||
final currentWorkspace = state.workspaces.firstWhereOrNull(
|
||||
(e) => e.workspaceId == state.currentWorkspace?.workspaceId,
|
||||
);
|
||||
final memberCount = currentWorkspace?.memberCount;
|
||||
String memberCountText = '';
|
||||
// if the member count is greater than 0, show the member count
|
||||
if (memberCount != null && memberCount > 0) {
|
||||
memberCountText = memberCount.toString();
|
||||
}
|
||||
return MobileSettingGroup(
|
||||
groupTitle: LocaleKeys.settings_appearance_members_label.tr(),
|
||||
settingItemList: [
|
||||
MobileSettingItem(
|
||||
name: LocaleKeys.settings_appearance_members_label.tr(),
|
||||
trailing: MobileSettingTrailing(
|
||||
text: memberCountText,
|
||||
),
|
||||
onTap: () {
|
||||
context.push(InviteMembersScreen.routeName);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
return MobileSettingGroup(
|
||||
groupTitle: LocaleKeys.settings_appearance_members_label.tr(),
|
||||
settingItemList: [
|
||||
MobileSettingItem(
|
||||
name: LocaleKeys.settings_appearance_members_label.tr(),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
onTap: () {
|
||||
context.push(InviteMembersScreen.routeName);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -72,7 +72,6 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
|
||||
final request = ViewIdPB(value: chatId);
|
||||
unawaited(FolderEventCloseView(request).send());
|
||||
selectedSourcesNotifier.dispose();
|
||||
chatController.dispose();
|
||||
return super.close();
|
||||
}
|
||||
|
||||
@@ -577,7 +576,6 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
|
||||
if (temporaryMessageIDMap.containsKey(messageId)) {
|
||||
messageId = temporaryMessageIDMap[messageId]!;
|
||||
}
|
||||
final metadata = message.metadata == 'null' ? '[]' : message.metadata;
|
||||
|
||||
return TextMessage(
|
||||
author: User(id: message.authorId),
|
||||
@@ -585,7 +583,7 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
|
||||
text: message.content,
|
||||
createdAt: message.createdAt.toDateTime(),
|
||||
metadata: {
|
||||
messageRefSourceJsonStringKey: metadata,
|
||||
messageRefSourceJsonStringKey: message.metadata,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ import 'dart:io';
|
||||
import 'package:appflowy/ai/ai.dart';
|
||||
import 'package:appflowy/plugins/ai_chat/presentation/chat_message_selector_banner.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_service.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/home_stack.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
|
||||
@@ -289,7 +288,7 @@ class _ChatContentPage extends StatelessWidget {
|
||||
return ChatMessage(
|
||||
message: message,
|
||||
animation: animation,
|
||||
padding: const EdgeInsets.symmetric(vertical: 18.0),
|
||||
padding: const EdgeInsets.symmetric(vertical: 12.0),
|
||||
receivedMessageScaleAnimationAlignment: Alignment.center,
|
||||
child: child,
|
||||
);
|
||||
@@ -414,7 +413,7 @@ class _InputState extends State<_Input> {
|
||||
return AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 150),
|
||||
transitionBuilder: (child, animation) {
|
||||
return NonClippingSizeTransition(
|
||||
return SizeTransition(
|
||||
sizeFactor: animation,
|
||||
axisAlignment: -1,
|
||||
child: child,
|
||||
|
||||
+1
-1
@@ -134,7 +134,7 @@ class _SaveToPageButtonState extends State<SaveToPageButton> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final userWorkspaceBloc = context.read<UserWorkspaceBloc>();
|
||||
final userProfile = userWorkspaceBloc.state.userProfile;
|
||||
final userProfile = userWorkspaceBloc.userProfile;
|
||||
final workspaceId =
|
||||
userWorkspaceBloc.state.currentWorkspace?.workspaceId ?? '';
|
||||
|
||||
|
||||
+2
-2
@@ -448,7 +448,7 @@ class _ChangeModelButtonState extends State<ChangeModelButton> {
|
||||
child: buildButton(context),
|
||||
popupBuilder: (_) {
|
||||
final bloc = context.read<AIPromptInputBloc>();
|
||||
final (models, _) = bloc.aiModelStateNotifier.getModelSelection();
|
||||
final (models, _) = bloc.aiModelStateNotifier.getAvailableModels();
|
||||
return SelectModelPopoverContent(
|
||||
models: models,
|
||||
selectedModel: null,
|
||||
@@ -516,7 +516,7 @@ class _SaveToPageButtonState extends State<SaveToPageButton> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final userWorkspaceBloc = context.read<UserWorkspaceBloc>();
|
||||
final userProfile = userWorkspaceBloc.state.userProfile;
|
||||
final userProfile = userWorkspaceBloc.userProfile;
|
||||
final workspaceId =
|
||||
userWorkspaceBloc.state.currentWorkspace?.workspaceId ?? '';
|
||||
|
||||
|
||||
+1
-1
@@ -407,7 +407,7 @@ class ChatAIMessagePopup extends StatelessWidget {
|
||||
return MobileQuickActionButton(
|
||||
onTap: () async {
|
||||
final bloc = context.read<AIPromptInputBloc>();
|
||||
final (models, _) = bloc.aiModelStateNotifier.getModelSelection();
|
||||
final (models, _) = bloc.aiModelStateNotifier.getAvailableModels();
|
||||
final result = await showChangeModelBottomSheet(context, models);
|
||||
if (result != null) {
|
||||
onChangeModel?.call(result);
|
||||
|
||||
-1
@@ -45,7 +45,6 @@ class ChatUserMessageBubble extends StatelessWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
const Spacer(),
|
||||
_buildBubble(context),
|
||||
const HSpace(DesktopAIChatSizes.avatarAndChatBubbleSpacing),
|
||||
_buildAvatar(),
|
||||
|
||||
+10
-11
@@ -3,9 +3,9 @@ import 'dart:async';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
|
||||
import 'package:appflowy/plugins/database/application/field/field_info.dart';
|
||||
import 'package:appflowy/plugins/database/application/field/type_option/type_option_data_parser.dart';
|
||||
import 'package:appflowy/plugins/database/domain/date_cell_service.dart';
|
||||
import 'package:appflowy/plugins/database/domain/field_service.dart';
|
||||
import 'package:appflowy/plugins/database/application/field/type_option/type_option_data_parser.dart';
|
||||
import 'package:appflowy/user/application/reminder/reminder_bloc.dart';
|
||||
import 'package:appflowy/user/application/reminder/reminder_extension.dart';
|
||||
import 'package:appflowy/util/int64_extension.dart';
|
||||
@@ -55,11 +55,10 @@ class DateCellEditorBloc
|
||||
didReceiveCellUpdate: (DateCellDataPB? cellData) {
|
||||
final dateCellData = DateCellData.fromPB(cellData);
|
||||
|
||||
ReminderOption reminderOption = state.reminderOption;
|
||||
ReminderOption reminderOption = ReminderOption.none;
|
||||
|
||||
if (dateCellData.reminderId.isNotEmpty &&
|
||||
dateCellData.dateTime != null &&
|
||||
reminderOption != ReminderOption.none) {
|
||||
dateCellData.dateTime != null) {
|
||||
final reminder = _reminderBloc.state.reminders
|
||||
.firstWhereOrNull((r) => r.id == dateCellData.reminderId);
|
||||
if (reminder != null) {
|
||||
@@ -113,15 +112,13 @@ class DateCellEditorBloc
|
||||
clearDate: () async {
|
||||
// Remove reminder if neccessary
|
||||
if (state.reminderId.isNotEmpty) {
|
||||
_reminderBloc.add(
|
||||
ReminderEvent.removeReminder(reminderId: state.reminderId),
|
||||
);
|
||||
_reminderBloc
|
||||
.add(ReminderEvent.remove(reminderId: state.reminderId));
|
||||
}
|
||||
|
||||
await _clearDate();
|
||||
},
|
||||
setReminderOption: (ReminderOption option) async {
|
||||
emit(state.copyWith(reminderOption: option));
|
||||
await _setReminderOption(option);
|
||||
},
|
||||
);
|
||||
@@ -219,7 +216,7 @@ class DateCellEditorBloc
|
||||
} else {
|
||||
if (option == ReminderOption.none) {
|
||||
// remove reminder from reminder bloc and cell data
|
||||
_reminderBloc.add(ReminderEvent.removeReminder(reminderId: state.reminderId));
|
||||
_reminderBloc.add(ReminderEvent.remove(reminderId: state.reminderId));
|
||||
await _updateCellReminderId("");
|
||||
} else {
|
||||
// Update reminder
|
||||
@@ -249,7 +246,9 @@ class DateCellEditorBloc
|
||||
void _updateReminderIfNecessary(
|
||||
DateTime? dateTime,
|
||||
) {
|
||||
if (state.reminderId.isEmpty || dateTime == null) {
|
||||
if (state.reminderId.isEmpty ||
|
||||
state.reminderOption == ReminderOption.none ||
|
||||
dateTime == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -401,7 +400,7 @@ class DateCellEditorState with _$DateCellEditorState {
|
||||
ReminderOption reminderOption = ReminderOption.none;
|
||||
|
||||
if (dateCellData.reminderId.isNotEmpty && dateCellData.dateTime != null) {
|
||||
final reminder = reminderBloc.state.allReminders
|
||||
final reminder = reminderBloc.state.reminders
|
||||
.firstWhereOrNull((r) => r.id == dateCellData.reminderId);
|
||||
if (reminder != null) {
|
||||
final eventDate = dateCellData.includeTime
|
||||
|
||||
+1
-1
@@ -31,7 +31,7 @@ class DatabaseSyncBloc extends Bloc<DatabaseSyncEvent, DatabaseSyncBlocState> {
|
||||
emit(
|
||||
state.copyWith(
|
||||
shouldShowIndicator:
|
||||
userProfile?.workspaceType == WorkspaceTypePB.ServerW &&
|
||||
userProfile?.workspaceAuthType == AuthTypePB.Server &&
|
||||
databaseId != null,
|
||||
),
|
||||
);
|
||||
|
||||
-1
@@ -280,7 +280,6 @@ class _ChecklistItemState extends State<ChecklistItem> {
|
||||
child: ChecklistCellTextfield(
|
||||
textController: textController,
|
||||
focusNode: textFieldFocusNode,
|
||||
lineHeight: Platform.isWindows ? 1.2 : 1.1,
|
||||
onChanged: () {
|
||||
_debounceOnChanged.call(() {
|
||||
if (!isComposing) {
|
||||
|
||||
+1
-6
@@ -46,7 +46,6 @@ class ChecklistCellTextfield extends StatelessWidget {
|
||||
horizontal: 2,
|
||||
),
|
||||
this.onSubmitted,
|
||||
this.lineHeight,
|
||||
});
|
||||
|
||||
final TextEditingController textController;
|
||||
@@ -54,17 +53,13 @@ class ChecklistCellTextfield extends StatelessWidget {
|
||||
final EdgeInsetsGeometry contentPadding;
|
||||
final VoidCallback? onSubmitted;
|
||||
final VoidCallback? onChanged;
|
||||
final double? lineHeight;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final textStyle = Theme.of(context).textTheme.bodyMedium;
|
||||
return TextField(
|
||||
controller: textController,
|
||||
focusNode: focusNode,
|
||||
style: textStyle?.copyWith(
|
||||
height: lineHeight,
|
||||
),
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
maxLines: null,
|
||||
decoration: InputDecoration(
|
||||
border: InputBorder.none,
|
||||
|
||||
@@ -69,8 +69,8 @@ class RowBanner extends StatefulWidget {
|
||||
class _RowBannerState extends State<RowBanner> {
|
||||
final _isHovering = ValueNotifier(false);
|
||||
late final isLocalMode =
|
||||
(widget.userProfile?.workspaceType ?? WorkspaceTypePB.LocalW) ==
|
||||
WorkspaceTypePB.LocalW;
|
||||
(widget.userProfile?.workspaceAuthType ?? AuthTypePB.Local) ==
|
||||
AuthTypePB.Local;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
|
||||
@@ -101,8 +101,8 @@ class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
|
||||
|
||||
bool get isLocalMode {
|
||||
final userProfilePB = state.userProfilePB;
|
||||
final type = userProfilePB?.workspaceType ?? WorkspaceTypePB.LocalW;
|
||||
return type == WorkspaceTypePB.LocalW;
|
||||
final type = userProfilePB?.workspaceAuthType ?? AuthTypePB.Local;
|
||||
return type == AuthTypePB.Local;
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
+1
-1
@@ -32,7 +32,7 @@ class DocumentCollaboratorsBloc
|
||||
emit(
|
||||
state.copyWith(
|
||||
shouldShowIndicator:
|
||||
userProfile?.workspaceType == WorkspaceTypePB.ServerW,
|
||||
userProfile?.workspaceAuthType == AuthTypePB.Server,
|
||||
),
|
||||
);
|
||||
final deviceId = ApplicationInfo.deviceId;
|
||||
|
||||
@@ -31,7 +31,7 @@ class DocumentSyncBloc extends Bloc<DocumentSyncEvent, DocumentSyncBlocState> {
|
||||
emit(
|
||||
state.copyWith(
|
||||
shouldShowIndicator:
|
||||
userProfile?.workspaceType == WorkspaceTypePB.ServerW,
|
||||
userProfile?.workspaceAuthType == AuthTypePB.Server,
|
||||
),
|
||||
);
|
||||
_syncStateListener.start(
|
||||
|
||||
+4
-10
@@ -68,13 +68,10 @@ Map<String, BlockComponentBuilder> buildBlockComponentBuilders({
|
||||
bool editable = true,
|
||||
ShowPlaceholder? showParagraphPlaceholder,
|
||||
String Function(Node)? placeholderText,
|
||||
EdgeInsets Function(Node node)? customPadding,
|
||||
EdgeInsets? customHeadingPadding,
|
||||
bool alwaysDistributeSimpleTableColumnWidths = false,
|
||||
}) {
|
||||
final configuration = _buildDefaultConfiguration(
|
||||
context,
|
||||
padding: customPadding,
|
||||
);
|
||||
final configuration = _buildDefaultConfiguration(context);
|
||||
final builders = _buildBlockComponentBuilderMap(
|
||||
context,
|
||||
configuration: configuration,
|
||||
@@ -100,10 +97,7 @@ Map<String, BlockComponentBuilder> buildBlockComponentBuilders({
|
||||
return builders;
|
||||
}
|
||||
|
||||
BlockComponentConfiguration _buildDefaultConfiguration(
|
||||
BuildContext context, {
|
||||
EdgeInsets Function(Node node)? padding,
|
||||
}) {
|
||||
BlockComponentConfiguration _buildDefaultConfiguration(BuildContext context) {
|
||||
final configuration = BlockComponentConfiguration(
|
||||
padding: (node) {
|
||||
if (UniversalPlatform.isMobile) {
|
||||
@@ -123,7 +117,7 @@ BlockComponentConfiguration _buildDefaultConfiguration(
|
||||
right: EditorStyleCustomizer.nodeHorizontalPadding,
|
||||
);
|
||||
}
|
||||
return padding?.call(node) ?? edgeInsets;
|
||||
return edgeInsets;
|
||||
}
|
||||
|
||||
return const EdgeInsets.symmetric(vertical: 5.0);
|
||||
|
||||
+3
-5
@@ -1,7 +1,6 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:appflowy/ai/ai.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_service.dart';
|
||||
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
@@ -29,14 +28,15 @@ class AiWriterCubit extends Cubit<AiWriterState> {
|
||||
this.onCreateNode,
|
||||
this.onRemoveNode,
|
||||
this.onAppendToDocument,
|
||||
}) : _aiService = getIt<AIRepository>(),
|
||||
AppFlowyAIService? aiService,
|
||||
}) : _aiService = aiService ?? AppFlowyAIService(),
|
||||
_textRobot = MarkdownTextRobot(editorState: editorState),
|
||||
selectedSourcesNotifier = ValueNotifier([documentId]),
|
||||
super(IdleAiWriterState());
|
||||
|
||||
final String documentId;
|
||||
final EditorState editorState;
|
||||
final AIRepository _aiService;
|
||||
final AppFlowyAIService _aiService;
|
||||
final MarkdownTextRobot _textRobot;
|
||||
final void Function()? onCreateNode;
|
||||
final void Function()? onRemoveNode;
|
||||
@@ -306,14 +306,12 @@ class AiWriterCubit extends Cubit<AiWriterState> {
|
||||
|
||||
// check the node is registered
|
||||
if (node == null) {
|
||||
Log.warn('[AI writer] Node is null');
|
||||
return (false, '');
|
||||
}
|
||||
|
||||
// check the selection is valid
|
||||
final selection = node.aiWriterSelection?.normalized;
|
||||
if (selection == null) {
|
||||
Log.warn('[AI writer]Selection is null');
|
||||
return (false, '');
|
||||
}
|
||||
|
||||
|
||||
-1
@@ -58,7 +58,6 @@ class _AiWriterScrollWrapperState extends State<AiWriterScrollWrapper> {
|
||||
@override
|
||||
void dispose() {
|
||||
aiWriterCubit.close();
|
||||
throttler.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
||||
+3
-10
@@ -31,22 +31,15 @@ Future<bool> _convertBacktickToCodeBlock({
|
||||
}
|
||||
|
||||
// only active when the backtick is at the beginning of the line
|
||||
final keyword = '``';
|
||||
final plainText = delta.toPlainText();
|
||||
if (!plainText.startsWith(keyword)) {
|
||||
if (plainText != '``') {
|
||||
return false;
|
||||
}
|
||||
|
||||
final transaction = editorState.transaction;
|
||||
final deltaWithoutKeyword = delta.compose(Delta()..delete(keyword.length));
|
||||
transaction.insertNodes(
|
||||
transaction.insertNode(
|
||||
selection.end.path,
|
||||
[
|
||||
codeBlockNode(
|
||||
delta: deltaWithoutKeyword,
|
||||
),
|
||||
if (node.children.isNotEmpty) ...node.children.map((e) => e.copyWith()),
|
||||
],
|
||||
codeBlockNode(),
|
||||
);
|
||||
transaction.deleteNode(node);
|
||||
transaction.afterSelection = Selection.collapsed(
|
||||
|
||||
-1
@@ -423,7 +423,6 @@ class _LinkEditMenuState extends State<LinkEditMenu> {
|
||||
return;
|
||||
}
|
||||
widget.onApply.call(linkInfo);
|
||||
onDismiss();
|
||||
}
|
||||
|
||||
void onConfirm() {
|
||||
|
||||
+5
-7
@@ -70,7 +70,6 @@ class LinkSearchTextField {
|
||||
bool showError = false,
|
||||
required BuildContext context,
|
||||
EdgeInsets contentPadding = const EdgeInsets.fromLTRB(8, 6, 8, 6),
|
||||
TextStyle? textStyle,
|
||||
}) {
|
||||
return TextFormField(
|
||||
autovalidateMode: AutovalidateMode.onUserInteraction,
|
||||
@@ -78,12 +77,11 @@ class LinkSearchTextField {
|
||||
focusNode: focusNode,
|
||||
textAlign: TextAlign.left,
|
||||
controller: textEditingController,
|
||||
style: textStyle ??
|
||||
TextStyle(
|
||||
fontSize: 14,
|
||||
height: 20 / 14,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
height: 20 / 14,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
onChanged: (text) {
|
||||
if (text.isEmpty) {
|
||||
searchedViews.clear();
|
||||
|
||||
Loaded 100 of 293 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user