mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2025-04-20 12:47:18 -04:00
Compare commits
5 commits
Author | SHA1 | Date | |
---|---|---|---|
|
c3bfae866b | ||
|
d3c7d4b1d2 | ||
|
8726df703e | ||
|
3fdd19f7a2 | ||
|
fb132cb4b8 |
59 changed files with 1018 additions and 140 deletions
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
|
@ -7,7 +7,7 @@ on:
|
|||
|
||||
env:
|
||||
FLUTTER_VERSION: "3.22.0"
|
||||
RUST_TOOLCHAIN: "1.77.2"
|
||||
RUST_TOOLCHAIN: "1.80.1"
|
||||
|
||||
jobs:
|
||||
create-release:
|
||||
|
|
2
.github/workflows/rust_ci.yaml
vendored
2
.github/workflows/rust_ci.yaml
vendored
|
@ -19,7 +19,7 @@ on:
|
|||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
CLOUD_VERSION: 0.7.6-amd64
|
||||
RUST_TOOLCHAIN: "1.77.2"
|
||||
RUST_TOOLCHAIN: "1.80.1"
|
||||
|
||||
jobs:
|
||||
self-hosted-job:
|
||||
|
|
2
.github/workflows/rust_coverage.yml
vendored
2
.github/workflows/rust_coverage.yml
vendored
|
@ -11,7 +11,7 @@ on:
|
|||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
FLUTTER_VERSION: "3.22.0"
|
||||
RUST_TOOLCHAIN: "1.77.2"
|
||||
RUST_TOOLCHAIN: "1.80.1"
|
||||
|
||||
jobs:
|
||||
tests:
|
||||
|
|
4
.github/workflows/tauri2_ci.yaml
vendored
4
.github/workflows/tauri2_ci.yaml
vendored
|
@ -11,7 +11,7 @@ on:
|
|||
env:
|
||||
NODE_VERSION: "18.16.0"
|
||||
PNPM_VERSION: "8.5.0"
|
||||
RUST_TOOLCHAIN: "1.77.2"
|
||||
RUST_TOOLCHAIN: "1.80.1"
|
||||
CARGO_MAKE_VERSION: "0.36.6"
|
||||
CI: true
|
||||
|
||||
|
@ -121,4 +121,4 @@ jobs:
|
|||
with:
|
||||
tauriScript: pnpm tauri
|
||||
projectPath: frontend/appflowy_web_app
|
||||
args: "--debug"
|
||||
args: "--debug"
|
||||
|
|
6
.github/workflows/tauri_ci.yaml
vendored
6
.github/workflows/tauri_ci.yaml
vendored
|
@ -7,7 +7,7 @@ on:
|
|||
env:
|
||||
NODE_VERSION: "18.16.0"
|
||||
PNPM_VERSION: "8.5.0"
|
||||
RUST_TOOLCHAIN: "1.77.2"
|
||||
RUST_TOOLCHAIN: "1.80.1"
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
|
@ -19,7 +19,7 @@ jobs:
|
|||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
platform: [ ubuntu-20.04 ]
|
||||
platform: [ubuntu-20.04]
|
||||
|
||||
runs-on: ${{ matrix.platform }}
|
||||
|
||||
|
@ -108,4 +108,4 @@ jobs:
|
|||
with:
|
||||
tauriScript: pnpm tauri
|
||||
projectPath: frontend/appflowy_tauri
|
||||
args: "--debug"
|
||||
args: "--debug"
|
||||
|
|
11
.github/workflows/tauri_release.yml
vendored
11
.github/workflows/tauri_release.yml
vendored
|
@ -4,20 +4,19 @@ on:
|
|||
workflow_dispatch:
|
||||
inputs:
|
||||
branch:
|
||||
description: 'The branch to release'
|
||||
description: "The branch to release"
|
||||
required: true
|
||||
default: 'main'
|
||||
default: "main"
|
||||
version:
|
||||
description: 'The version to release'
|
||||
description: "The version to release"
|
||||
required: true
|
||||
default: '0.0.0'
|
||||
default: "0.0.0"
|
||||
env:
|
||||
NODE_VERSION: "18.16.0"
|
||||
PNPM_VERSION: "8.5.0"
|
||||
RUST_TOOLCHAIN: "1.77.2"
|
||||
RUST_TOOLCHAIN: "1.80.1"
|
||||
|
||||
jobs:
|
||||
|
||||
publish-tauri:
|
||||
permissions:
|
||||
contents: write
|
||||
|
|
|
@ -1,4 +1,8 @@
|
|||
# Release Notes
|
||||
## Version 0.7.6 - 03/12/2024
|
||||
### Bug Fixes
|
||||
|
||||
|
||||
## Version 0.7.6 - 03/12/2024
|
||||
### New Features
|
||||
- Revamped the simple table UI
|
||||
|
|
|
@ -26,7 +26,7 @@ CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true
|
|||
CARGO_MAKE_CRATE_FS_NAME = "dart_ffi"
|
||||
CARGO_MAKE_CRATE_NAME = "dart-ffi"
|
||||
LIB_NAME = "dart_ffi"
|
||||
APPFLOWY_VERSION = "0.7.6"
|
||||
APPFLOWY_VERSION = "0.7.7"
|
||||
FLUTTER_DESKTOP_FEATURES = "dart"
|
||||
PRODUCT_NAME = "AppFlowy"
|
||||
MACOSX_DEPLOYMENT_TARGET = "11.0"
|
||||
|
|
|
@ -301,6 +301,7 @@ void main() {
|
|||
await tester.createOption(name: "qwer");
|
||||
await tester.selectOption(name: "asdf");
|
||||
await tester.dismissCellEditor();
|
||||
await tester.dismissCellEditor();
|
||||
|
||||
await tester.tapDatabaseFilterButton();
|
||||
await tester.tapCreateFilterByFieldType(FieldType.MultiSelect, "Tags");
|
||||
|
@ -332,6 +333,7 @@ void main() {
|
|||
await tester.tapButton(finderForFieldType(FieldType.MultiSelect));
|
||||
await tester.selectOption(name: "asdf");
|
||||
await tester.dismissCellEditor();
|
||||
await tester.dismissCellEditor();
|
||||
|
||||
tester.assertNumberOfEventsInCalendar(0);
|
||||
|
||||
|
|
|
@ -2,12 +2,12 @@ import 'dart:io';
|
|||
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart';
|
||||
import 'package:appflowy/plugins/inline_actions/inline_actions_menu.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/view/view_action_type.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:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
|
@ -330,6 +330,23 @@ void main() {
|
|||
expect(find.text("$_createdPageName (copy)"), findsNWidgets(2));
|
||||
expect(find.text("$_createdPageName (copy) (copy)"), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('Cancel inline page reference menu by space', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
await tester.createOpenRenameDocumentUnderParent(name: _firstDocName);
|
||||
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
await tester.editor.showPlusMenu();
|
||||
|
||||
// Cancel by space
|
||||
await tester.simulateKeyEvent(
|
||||
LogicalKeyboardKey.space,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byType(InlineActionsMenu), findsNothing);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -1,14 +1,21 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_date_block.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/user/application/reminder/reminder_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/settings/date_time/date_format_ext.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/date_picker/desktop_date_picker.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
import 'package:table_calendar/table_calendar.dart';
|
||||
|
||||
import '../../shared/util.dart';
|
||||
|
||||
|
@ -18,7 +25,7 @@ void main() {
|
|||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
});
|
||||
|
||||
group('date or reminder block in document', () {
|
||||
group('date or reminder block in document:', () {
|
||||
testWidgets("insert date with time block", (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
|
@ -121,5 +128,339 @@ void main() {
|
|||
expect(find.text('@$formattedDate'), findsOneWidget);
|
||||
expect(find.byFlowySvg(FlowySvgs.reminder_clock_s), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets("copy, cut and paste a date mention", (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
|
||||
// create a new document
|
||||
await tester.createNewPageWithNameUnderParent(
|
||||
name: 'copy, cut and paste a date mention',
|
||||
);
|
||||
|
||||
// tap the first line of the document
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
await tester.editor.showSlashMenu();
|
||||
await tester.editor.tapSlashMenuItemWithName(
|
||||
LocaleKeys.document_slashMenu_name_dateOrReminder.tr(),
|
||||
);
|
||||
|
||||
final dateTimeSettings = DateTimeSettingsPB(
|
||||
dateFormat: UserDateFormatPB.Friendly,
|
||||
timeFormat: UserTimeFormatPB.TwentyFourHour,
|
||||
);
|
||||
final DateTime currentDateTime = DateTime.now();
|
||||
final String formattedDate =
|
||||
dateTimeSettings.dateFormat.formatDate(currentDateTime, false);
|
||||
|
||||
// get current date in editor
|
||||
expect(find.byType(MentionDateBlock), findsOneWidget);
|
||||
expect(find.text('@$formattedDate'), findsOneWidget);
|
||||
|
||||
// update selection and copy
|
||||
await tester.editor.updateSelection(
|
||||
Selection(
|
||||
start: Position(path: [0]),
|
||||
end: Position(path: [0], offset: 1),
|
||||
),
|
||||
);
|
||||
await tester.simulateKeyEvent(
|
||||
LogicalKeyboardKey.keyC,
|
||||
isControlPressed: Platform.isLinux || Platform.isWindows,
|
||||
isMetaPressed: Platform.isMacOS,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// update selection and paste
|
||||
await tester.editor.updateSelection(
|
||||
Selection.collapsed(Position(path: [0], offset: 1)),
|
||||
);
|
||||
await tester.simulateKeyEvent(
|
||||
LogicalKeyboardKey.keyV,
|
||||
isControlPressed: Platform.isLinux || Platform.isWindows,
|
||||
isMetaPressed: Platform.isMacOS,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byType(MentionDateBlock), findsNWidgets(2));
|
||||
expect(find.text('@$formattedDate'), findsNWidgets(2));
|
||||
|
||||
// update selection and cut
|
||||
await tester.editor.updateSelection(
|
||||
Selection(
|
||||
start: Position(path: [0], offset: 1),
|
||||
end: Position(path: [0], offset: 2),
|
||||
),
|
||||
);
|
||||
await tester.simulateKeyEvent(
|
||||
LogicalKeyboardKey.keyX,
|
||||
isControlPressed: Platform.isLinux || Platform.isWindows,
|
||||
isMetaPressed: Platform.isMacOS,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byType(MentionDateBlock), findsOneWidget);
|
||||
expect(find.text('@$formattedDate'), findsOneWidget);
|
||||
|
||||
// update selection and paste
|
||||
await tester.editor.updateSelection(
|
||||
Selection.collapsed(Position(path: [0], offset: 1)),
|
||||
);
|
||||
await tester.simulateKeyEvent(
|
||||
LogicalKeyboardKey.keyV,
|
||||
isControlPressed: Platform.isLinux || Platform.isWindows,
|
||||
isMetaPressed: Platform.isMacOS,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byType(MentionDateBlock), findsNWidgets(2));
|
||||
expect(find.text('@$formattedDate'), findsNWidgets(2));
|
||||
});
|
||||
|
||||
testWidgets("copy, cut and paste a reminder mention", (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
|
||||
// create a new document
|
||||
await tester.createNewPageWithNameUnderParent(
|
||||
name: 'copy, cut and paste a reminder mention',
|
||||
);
|
||||
|
||||
// tap the first line of the document
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
await tester.editor.showSlashMenu();
|
||||
await tester.editor.tapSlashMenuItemWithName(
|
||||
LocaleKeys.document_slashMenu_name_dateOrReminder.tr(),
|
||||
);
|
||||
|
||||
// trigger popup
|
||||
await tester.tapButton(find.byType(MentionDateBlock));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// set date to be fifteenth of the next month
|
||||
await tester.tap(
|
||||
find.descendant(
|
||||
of: find.byType(DesktopAppFlowyDatePicker),
|
||||
matching: find.byFlowySvg(FlowySvgs.arrow_right_s),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(
|
||||
find.descendant(
|
||||
of: find.byType(TableCalendar),
|
||||
matching: find.text(15.toString()),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.simulateKeyEvent(LogicalKeyboardKey.escape);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// add a reminder
|
||||
await tester.tap(find.byType(MentionDateBlock));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text(LocaleKeys.datePicker_reminderLabel.tr()));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(
|
||||
find.textContaining(
|
||||
LocaleKeys.datePicker_reminderOptions_oneDayBefore.tr(),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.simulateKeyEvent(LogicalKeyboardKey.escape);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// verify
|
||||
final dateTimeSettings = DateTimeSettingsPB(
|
||||
dateFormat: UserDateFormatPB.Friendly,
|
||||
timeFormat: UserTimeFormatPB.TwentyFourHour,
|
||||
);
|
||||
final now = DateTime.now();
|
||||
final fifteenthOfNextMonth = DateTime(now.year, now.month + 1, 15);
|
||||
final formattedDate =
|
||||
dateTimeSettings.dateFormat.formatDate(fifteenthOfNextMonth, false);
|
||||
|
||||
expect(find.byType(MentionDateBlock), findsOneWidget);
|
||||
expect(find.text('@$formattedDate'), findsOneWidget);
|
||||
expect(find.byFlowySvg(FlowySvgs.reminder_clock_s), findsOneWidget);
|
||||
expect(getIt<ReminderBloc>().state.reminders.map((e) => e.id).length, 1);
|
||||
|
||||
// update selection and copy
|
||||
await tester.editor.updateSelection(
|
||||
Selection(
|
||||
start: Position(path: [0]),
|
||||
end: Position(path: [0], offset: 1),
|
||||
),
|
||||
);
|
||||
await tester.simulateKeyEvent(
|
||||
LogicalKeyboardKey.keyC,
|
||||
isControlPressed: Platform.isLinux || Platform.isWindows,
|
||||
isMetaPressed: Platform.isMacOS,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// update selection and paste
|
||||
await tester.editor.updateSelection(
|
||||
Selection.collapsed(Position(path: [0], offset: 1)),
|
||||
);
|
||||
await tester.simulateKeyEvent(
|
||||
LogicalKeyboardKey.keyV,
|
||||
isControlPressed: Platform.isLinux || Platform.isWindows,
|
||||
isMetaPressed: Platform.isMacOS,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byType(MentionDateBlock), findsNWidgets(2));
|
||||
expect(find.text('@$formattedDate'), findsNWidgets(2));
|
||||
expect(find.byFlowySvg(FlowySvgs.reminder_clock_s), findsNWidgets(2));
|
||||
expect(
|
||||
getIt<ReminderBloc>().state.reminders.map((e) => e.id).toSet().length,
|
||||
2,
|
||||
);
|
||||
|
||||
// update selection and cut
|
||||
await tester.editor.updateSelection(
|
||||
Selection(
|
||||
start: Position(path: [0], offset: 1),
|
||||
end: Position(path: [0], offset: 2),
|
||||
),
|
||||
);
|
||||
await tester.simulateKeyEvent(
|
||||
LogicalKeyboardKey.keyX,
|
||||
isControlPressed: Platform.isLinux || Platform.isWindows,
|
||||
isMetaPressed: Platform.isMacOS,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byType(MentionDateBlock), findsOneWidget);
|
||||
expect(find.text('@$formattedDate'), findsOneWidget);
|
||||
expect(find.byFlowySvg(FlowySvgs.reminder_clock_s), findsOneWidget);
|
||||
expect(getIt<ReminderBloc>().state.reminders.map((e) => e.id).length, 1);
|
||||
|
||||
// update selection and paste
|
||||
await tester.editor.updateSelection(
|
||||
Selection.collapsed(Position(path: [0], offset: 1)),
|
||||
);
|
||||
await tester.simulateKeyEvent(
|
||||
LogicalKeyboardKey.keyV,
|
||||
isControlPressed: Platform.isLinux || Platform.isWindows,
|
||||
isMetaPressed: Platform.isMacOS,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byType(MentionDateBlock), findsNWidgets(2));
|
||||
expect(find.text('@$formattedDate'), findsNWidgets(2));
|
||||
expect(find.byType(MentionDateBlock), findsNWidgets(2));
|
||||
expect(find.text('@$formattedDate'), findsNWidgets(2));
|
||||
expect(find.byFlowySvg(FlowySvgs.reminder_clock_s), findsNWidgets(2));
|
||||
expect(
|
||||
getIt<ReminderBloc>().state.reminders.map((e) => e.id).toSet().length,
|
||||
2,
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets("delete, undo and redo a reminder mention", (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
|
||||
// create a new document
|
||||
await tester.createNewPageWithNameUnderParent(
|
||||
name: 'delete, undo and redo a reminder mention',
|
||||
);
|
||||
|
||||
// tap the first line of the document
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
await tester.editor.showSlashMenu();
|
||||
await tester.editor.tapSlashMenuItemWithName(
|
||||
LocaleKeys.document_slashMenu_name_dateOrReminder.tr(),
|
||||
);
|
||||
|
||||
// trigger popup
|
||||
await tester.tapButton(find.byType(MentionDateBlock));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// set date to be fifteenth of the next month
|
||||
await tester.tap(
|
||||
find.descendant(
|
||||
of: find.byType(DesktopAppFlowyDatePicker),
|
||||
matching: find.byFlowySvg(FlowySvgs.arrow_right_s),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(
|
||||
find.descendant(
|
||||
of: find.byType(TableCalendar),
|
||||
matching: find.text(15.toString()),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.simulateKeyEvent(LogicalKeyboardKey.escape);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// add a reminder
|
||||
await tester.tap(find.byType(MentionDateBlock));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text(LocaleKeys.datePicker_reminderLabel.tr()));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(
|
||||
find.textContaining(
|
||||
LocaleKeys.datePicker_reminderOptions_oneDayBefore.tr(),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.simulateKeyEvent(LogicalKeyboardKey.escape);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// verify
|
||||
final dateTimeSettings = DateTimeSettingsPB(
|
||||
dateFormat: UserDateFormatPB.Friendly,
|
||||
timeFormat: UserTimeFormatPB.TwentyFourHour,
|
||||
);
|
||||
final now = DateTime.now();
|
||||
final fifteenthOfNextMonth = DateTime(now.year, now.month + 1, 15);
|
||||
final formattedDate =
|
||||
dateTimeSettings.dateFormat.formatDate(fifteenthOfNextMonth, false);
|
||||
|
||||
expect(find.byType(MentionDateBlock), findsOneWidget);
|
||||
expect(find.text('@$formattedDate'), findsOneWidget);
|
||||
expect(find.byFlowySvg(FlowySvgs.reminder_clock_s), findsOneWidget);
|
||||
expect(getIt<ReminderBloc>().state.reminders.map((e) => e.id).length, 1);
|
||||
|
||||
// update selection and backspace to delete the mention
|
||||
await tester.editor.updateSelection(
|
||||
Selection.collapsed(Position(path: [0], offset: 1)),
|
||||
);
|
||||
await tester.simulateKeyEvent(LogicalKeyboardKey.backspace);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byType(MentionDateBlock), findsNothing);
|
||||
expect(find.text('@$formattedDate'), findsNothing);
|
||||
expect(find.byFlowySvg(FlowySvgs.reminder_clock_s), findsNothing);
|
||||
expect(getIt<ReminderBloc>().state.reminders.isEmpty, isTrue);
|
||||
|
||||
// undo
|
||||
await tester.simulateKeyEvent(
|
||||
LogicalKeyboardKey.keyZ,
|
||||
isControlPressed: Platform.isWindows || Platform.isLinux,
|
||||
isMetaPressed: Platform.isMacOS,
|
||||
);
|
||||
|
||||
expect(find.byType(MentionDateBlock), findsOneWidget);
|
||||
expect(find.text('@$formattedDate'), findsOneWidget);
|
||||
expect(find.byFlowySvg(FlowySvgs.reminder_clock_s), findsOneWidget);
|
||||
expect(getIt<ReminderBloc>().state.reminders.map((e) => e.id).length, 1);
|
||||
|
||||
// redo
|
||||
await tester.simulateKeyEvent(
|
||||
LogicalKeyboardKey.keyZ,
|
||||
isControlPressed: Platform.isWindows || Platform.isLinux,
|
||||
isMetaPressed: Platform.isMacOS,
|
||||
isShiftPressed: true,
|
||||
);
|
||||
|
||||
expect(find.byType(MentionDateBlock), findsNothing);
|
||||
expect(find.text('@$formattedDate'), findsNothing);
|
||||
expect(find.byFlowySvg(FlowySvgs.reminder_clock_s), findsNothing);
|
||||
expect(getIt<ReminderBloc>().state.reminders.isEmpty, isTrue);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,16 +1,24 @@
|
|||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/shared/patterns/common_patterns.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/toast.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:open_filex/open_filex.dart';
|
||||
import 'package:string_validator/string_validator.dart';
|
||||
import 'package:url_launcher/url_launcher.dart' as launcher;
|
||||
|
||||
typedef OnFailureCallback = void Function(Uri uri);
|
||||
|
||||
Future<bool> afLaunchUrl(
|
||||
/// Launch the uri
|
||||
///
|
||||
/// If the uri is a local file path, it will be opened with the OpenFilex.
|
||||
/// Otherwise, it will be launched with the url_launcher.
|
||||
Future<bool> afLaunchUri(
|
||||
Uri uri, {
|
||||
BuildContext? context,
|
||||
OnFailureCallback? onFailure,
|
||||
|
@ -18,6 +26,18 @@ Future<bool> afLaunchUrl(
|
|||
String? webOnlyWindowName,
|
||||
bool addingHttpSchemeWhenFailed = false,
|
||||
}) async {
|
||||
final url = uri.toString();
|
||||
final decodedUrl = Uri.decodeComponent(url);
|
||||
|
||||
// check if the uri is the local file path
|
||||
if (localPathRegex.hasMatch(decodedUrl)) {
|
||||
return _afLaunchLocalUri(
|
||||
uri,
|
||||
context: context,
|
||||
onFailure: onFailure,
|
||||
);
|
||||
}
|
||||
|
||||
// try to launch the uri directly
|
||||
bool result;
|
||||
try {
|
||||
|
@ -32,7 +52,7 @@ Future<bool> afLaunchUrl(
|
|||
}
|
||||
|
||||
// if the uri is not a valid url, try to launch it with http scheme
|
||||
final url = uri.toString();
|
||||
|
||||
if (addingHttpSchemeWhenFailed &&
|
||||
!result &&
|
||||
!isURL(url, {'require_protocol': true})) {
|
||||
|
@ -54,9 +74,14 @@ Future<bool> afLaunchUrl(
|
|||
return result;
|
||||
}
|
||||
|
||||
/// Launch the url string
|
||||
///
|
||||
/// See [afLaunchUri] for more details.
|
||||
Future<bool> afLaunchUrlString(
|
||||
String url, {
|
||||
bool addingHttpSchemeWhenFailed = false,
|
||||
BuildContext? context,
|
||||
OnFailureCallback? onFailure,
|
||||
}) async {
|
||||
final Uri uri;
|
||||
try {
|
||||
|
@ -67,12 +92,56 @@ Future<bool> afLaunchUrlString(
|
|||
}
|
||||
|
||||
// try to launch the uri directly
|
||||
return afLaunchUrl(
|
||||
return afLaunchUri(
|
||||
uri,
|
||||
addingHttpSchemeWhenFailed: addingHttpSchemeWhenFailed,
|
||||
context: context,
|
||||
onFailure: onFailure,
|
||||
);
|
||||
}
|
||||
|
||||
/// Launch the local uri
|
||||
///
|
||||
/// See [afLaunchUri] for more details.
|
||||
Future<bool> _afLaunchLocalUri(
|
||||
Uri uri, {
|
||||
BuildContext? context,
|
||||
OnFailureCallback? onFailure,
|
||||
}) async {
|
||||
final decodedUrl = Uri.decodeComponent(uri.toString());
|
||||
// open the file with the OpenfileX
|
||||
var result = await OpenFilex.open(decodedUrl);
|
||||
if (result.type != ResultType.done) {
|
||||
// For the file cant be opened, fallback to open the folder
|
||||
final parentFolder = Directory(decodedUrl).parent.path;
|
||||
result = await OpenFilex.open(parentFolder);
|
||||
}
|
||||
// show the toast if the file is not found
|
||||
final message = switch (result.type) {
|
||||
ResultType.done => LocaleKeys.openFileMessage_success.tr(),
|
||||
ResultType.fileNotFound => LocaleKeys.openFileMessage_fileNotFound.tr(),
|
||||
ResultType.noAppToOpen => LocaleKeys.openFileMessage_noAppToOpenFile.tr(),
|
||||
ResultType.permissionDenied =>
|
||||
LocaleKeys.openFileMessage_permissionDenied.tr(),
|
||||
ResultType.error => LocaleKeys.failedToOpenUrl.tr(),
|
||||
};
|
||||
if (context != null && context.mounted) {
|
||||
showToastNotification(
|
||||
context,
|
||||
message: message,
|
||||
type: result.type == ResultType.done
|
||||
? ToastificationType.success
|
||||
: ToastificationType.error,
|
||||
);
|
||||
}
|
||||
final openFileSuccess = result.type == ResultType.done;
|
||||
if (!openFileSuccess && onFailure != null) {
|
||||
onFailure(uri);
|
||||
Log.error('Failed to open file: $result.message');
|
||||
}
|
||||
return openFileSuccess;
|
||||
}
|
||||
|
||||
void _errorHandler(
|
||||
Uri uri, {
|
||||
BuildContext? context,
|
||||
|
|
|
@ -189,7 +189,7 @@ class MobileViewPageMoreBottomSheet extends StatelessWidget {
|
|||
final url = context.read<ShareBloc>().state.url;
|
||||
if (url.isNotEmpty) {
|
||||
unawaited(
|
||||
afLaunchUrl(
|
||||
afLaunchUri(
|
||||
Uri.parse(url),
|
||||
mode: LaunchMode.externalApplication,
|
||||
),
|
||||
|
|
|
@ -59,6 +59,16 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
|
|||
AnswerStream? answerStream;
|
||||
int numSendMessage = 0;
|
||||
|
||||
/// a counter used to determine whether the initial loading state should be
|
||||
/// set to finish. It should hit two before we emit: one for the local fetch
|
||||
/// and another for the server fetch.
|
||||
///
|
||||
/// This is to work around a bug where if an ai chat that is not yet on the
|
||||
/// user local storage is opened but has messages in the server, it will
|
||||
/// remain stuck on the welcome screen until the user switches to another page
|
||||
/// then come back.
|
||||
int initialFetchCounter = 0;
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
await answerStream?.dispose();
|
||||
|
@ -76,7 +86,11 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
|
|||
await chatController.insert(message, index: 0);
|
||||
}
|
||||
|
||||
if (state.loadingState.isLoading) {
|
||||
if (initialFetchCounter < 2) {
|
||||
initialFetchCounter++;
|
||||
}
|
||||
|
||||
if (state.loadingState.isLoading && initialFetchCounter >= 2) {
|
||||
emit(
|
||||
state.copyWith(loadingState: const ChatLoadingState.finish()),
|
||||
);
|
||||
|
|
|
@ -1,24 +1,23 @@
|
|||
import 'package:appflowy/core/helpers/url_launcher.dart';
|
||||
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet_media_upload.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/common.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/image_viewer/image_provider.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/image_viewer/interactive_image_viewer.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
|
||||
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet_media_upload.dart';
|
||||
import 'package:appflowy/plugins/database/application/cell/bloc/media_cell_bloc.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/media.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/cell_editor/media_cell_editor.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/cell_editor/mobile_media_cell_editor.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/media_file_type_ext.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/common.dart';
|
||||
import 'package:appflowy/shared/af_image.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/image_viewer/image_provider.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/image_viewer/interactive_image_viewer.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/media_entities.pb.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/theme_extension.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:universal_platform/universal_platform.dart';
|
||||
|
||||
|
@ -159,7 +158,7 @@ class GridMediaCellSkin extends IEditableMediaCellSkin {
|
|||
List<MediaFilePB> files,
|
||||
) {
|
||||
if (file.fileType != MediaFileTypePB.Image) {
|
||||
afLaunchUrlString(file.url);
|
||||
afLaunchUrlString(file.url, context: context);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -31,6 +31,7 @@ class DesktopRowDetailDateCellSkin extends IEditableDateCellSkin {
|
|||
direction: PopoverDirection.bottomWithLeftAligned,
|
||||
constraints: BoxConstraints.loose(const Size(260, 620)),
|
||||
margin: EdgeInsets.zero,
|
||||
asBarrier: true,
|
||||
child: Container(
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
|
||||
|
|
|
@ -27,7 +27,6 @@ import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
|||
import 'package:flowy_infra_ui/style_widget/hover.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:open_filex/open_filex.dart';
|
||||
import 'package:reorderables/reorderables.dart';
|
||||
|
||||
const _dropFileKey = 'files_media';
|
||||
|
@ -253,6 +252,7 @@ class _AddFileButtonState extends State<_AddFileButton> {
|
|||
direction: widget.direction,
|
||||
constraints: const BoxConstraints(maxWidth: _menuWidth),
|
||||
margin: EdgeInsets.zero,
|
||||
asBarrier: true,
|
||||
onClose: () =>
|
||||
context.read<EditorDropManagerState>().remove(_dropFileKey),
|
||||
popupBuilder: (_) {
|
||||
|
@ -459,6 +459,7 @@ class _FilePreviewRenderState extends State<_FilePreviewRender> {
|
|||
offset: const Offset(0, 5),
|
||||
triggerActions: PopoverTriggerFlags.none,
|
||||
onClose: () => setState(() => isSelected = false),
|
||||
asBarrier: true,
|
||||
popupBuilder: (popoverContext) => MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider.value(value: context.read<RowDetailBloc>()),
|
||||
|
@ -479,7 +480,7 @@ class _FilePreviewRenderState extends State<_FilePreviewRender> {
|
|||
? null
|
||||
: () {
|
||||
if (file.uploadType == FileUploadTypePB.LocalFile) {
|
||||
OpenFilex.open(file.url);
|
||||
afLaunchUrlString(file.url);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -24,6 +24,7 @@ class DesktopRowDetailRelationCellSkin extends IEditableRelationCellSkin {
|
|||
direction: PopoverDirection.bottomWithLeftAligned,
|
||||
constraints: const BoxConstraints(maxWidth: 400, maxHeight: 400),
|
||||
margin: EdgeInsets.zero,
|
||||
asBarrier: true,
|
||||
onClose: () => cellContainerNotifier.isFocus = false,
|
||||
popupBuilder: (context) {
|
||||
return BlocProvider.value(
|
||||
|
|
|
@ -25,6 +25,7 @@ class DesktopRowDetailSelectOptionCellSkin
|
|||
controller: popoverController,
|
||||
constraints: const BoxConstraints.tightFor(width: 300),
|
||||
margin: EdgeInsets.zero,
|
||||
asBarrier: true,
|
||||
triggerActions: PopoverTriggerFlags.none,
|
||||
direction: PopoverDirection.bottomWithLeftAligned,
|
||||
onClose: () => cellContainerNotifier.isFocus = false,
|
||||
|
|
|
@ -199,22 +199,30 @@ class FieldActionCell extends StatelessWidget {
|
|||
(action == FieldAction.duplicate || action == FieldAction.delete)) {
|
||||
enable = false;
|
||||
}
|
||||
|
||||
return FlowyButton(
|
||||
return FlowyIconTextButton(
|
||||
resetHoverOnRebuild: false,
|
||||
disable: !enable,
|
||||
text: FlowyText(
|
||||
action.title(fieldInfo),
|
||||
lineHeight: 1.0,
|
||||
color: enable ? null : Theme.of(context).disabledColor,
|
||||
),
|
||||
onHover: (_) => popoverMutex?.close(),
|
||||
onTap: () => action.run(context, viewId, fieldInfo),
|
||||
leftIcon: action.leading(
|
||||
fieldInfo,
|
||||
enable ? null : Theme.of(context).disabledColor,
|
||||
// show the error color when delete is hovered
|
||||
textBuilder: (onHover) => FlowyText(
|
||||
action.title(fieldInfo),
|
||||
lineHeight: 1.0,
|
||||
color: enable
|
||||
? action == FieldAction.delete && onHover
|
||||
? Theme.of(context).colorScheme.error
|
||||
: null
|
||||
: Theme.of(context).disabledColor,
|
||||
),
|
||||
rightIcon: action.trailing(context, fieldInfo),
|
||||
leftIconBuilder: (onHover) => action.leading(
|
||||
fieldInfo,
|
||||
enable
|
||||
? action == FieldAction.delete && onHover
|
||||
? Theme.of(context).colorScheme.error
|
||||
: null
|
||||
: Theme.of(context).disabledColor,
|
||||
),
|
||||
rightIconBuilder: (_) => action.trailing(context, fieldInfo),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -117,6 +117,22 @@ Future<bool> inlinePageReferenceCommandHandler(
|
|||
initialResults: initialResults,
|
||||
style: style,
|
||||
startCharAmount: previousChar != null ? 2 : 1,
|
||||
cancelBySpaceHandler: () {
|
||||
if (character == _plusChar) {
|
||||
final currentSelection = editorState.selection;
|
||||
if (currentSelection == null) {
|
||||
return false;
|
||||
}
|
||||
// check if the space is after the character
|
||||
if (currentSelection.isCollapsed &&
|
||||
currentSelection.start.offset ==
|
||||
selection.start.offset + character.length) {
|
||||
_cancelInlinePageReferenceMenu(editorState);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
},
|
||||
);
|
||||
|
||||
selectionMenuService?.show();
|
||||
|
@ -124,3 +140,17 @@ Future<bool> inlinePageReferenceCommandHandler(
|
|||
|
||||
return true;
|
||||
}
|
||||
|
||||
void _cancelInlinePageReferenceMenu(EditorState editorState) {
|
||||
selectionMenuService?.dismiss();
|
||||
selectionMenuService = null;
|
||||
|
||||
// re-focus the selection
|
||||
final selection = editorState.selection;
|
||||
if (selection != null) {
|
||||
editorState.updateSelectionWithReason(
|
||||
selection,
|
||||
reason: SelectionUpdateReason.uiEvent,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,7 +10,6 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_p
|
|||
import 'package:appflowy/plugins/document/presentation/editor_plugins/file/file_util.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/toast.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:cross_file/cross_file.dart';
|
||||
import 'package:desktop_drop/desktop_drop.dart';
|
||||
|
@ -19,7 +18,6 @@ import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
|||
import 'package:flowy_infra_ui/style_widget/hover.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:open_filex/open_filex.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:string_validator/string_validator.dart';
|
||||
import 'package:universal_platform/universal_platform.dart';
|
||||
|
@ -323,22 +321,7 @@ class FileBlockComponentState extends State<FileBlockComponent>
|
|||
FileUrlType urlType,
|
||||
String url,
|
||||
) async {
|
||||
if ([FileUrlType.cloud, FileUrlType.network].contains(urlType)) {
|
||||
await afLaunchUrlString(url);
|
||||
} else {
|
||||
final result = await OpenFilex.open(url);
|
||||
if (result.type == ResultType.done) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (context.mounted) {
|
||||
showToastNotification(
|
||||
context,
|
||||
message: LocaleKeys.document_plugins_file_failedToOpenMsg.tr(),
|
||||
type: ToastificationType.error,
|
||||
);
|
||||
}
|
||||
}
|
||||
await afLaunchUrlString(url, context: context);
|
||||
}
|
||||
|
||||
void _openMenu() {
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/core/helpers/url_launcher.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/application/document_service.dart';
|
||||
|
@ -22,6 +20,7 @@ import 'package:cross_file/cross_file.dart';
|
|||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/file_picker/file_picker_impl.dart';
|
||||
import 'package:flowy_infra/uuid.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:universal_platform/universal_platform.dart';
|
||||
|
@ -102,7 +101,7 @@ Future<void> downloadMediaFile(
|
|||
FileUploadTypePB.LocalFile,
|
||||
].contains(file.uploadType)) {
|
||||
/// When the file is a network file or a local file, we can directly open the file.
|
||||
await afLaunchUrl(Uri.parse(file.url));
|
||||
await afLaunchUrlString(file.url);
|
||||
} else {
|
||||
if (userProfile == null) {
|
||||
return showToastNotification(
|
||||
|
|
|
@ -16,11 +16,12 @@ import '../transaction_handler/mention_transaction_handler.dart';
|
|||
const _pasteIdentifier = 'child_page_transaction';
|
||||
|
||||
class ChildPageTransactionHandler extends MentionTransactionHandler {
|
||||
ChildPageTransactionHandler() : super(subType: MentionType.childPage.name);
|
||||
ChildPageTransactionHandler();
|
||||
|
||||
@override
|
||||
Future<void> onTransaction(
|
||||
BuildContext context,
|
||||
String viewId,
|
||||
EditorState editorState,
|
||||
List<MentionBlockData> added,
|
||||
List<MentionBlockData> removed, {
|
||||
|
|
|
@ -0,0 +1,263 @@
|
|||
import 'package:appflowy/shared/clipboard_state.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/user/application/reminder/reminder_bloc.dart';
|
||||
import 'package:appflowy/user/application/reminder/reminder_extension.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/reminder_selector.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:fixnum/fixnum.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:nanoid/nanoid.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../plugins.dart';
|
||||
import '../transaction_handler/mention_transaction_handler.dart';
|
||||
|
||||
const _pasteIdentifier = 'date_transaction';
|
||||
|
||||
class DateTransactionHandler extends MentionTransactionHandler {
|
||||
DateTransactionHandler();
|
||||
|
||||
@override
|
||||
Future<void> onTransaction(
|
||||
BuildContext context,
|
||||
String viewId,
|
||||
EditorState editorState,
|
||||
List<MentionBlockData> added,
|
||||
List<MentionBlockData> removed, {
|
||||
bool isCut = false,
|
||||
bool isUndoRedo = false,
|
||||
bool isPaste = false,
|
||||
bool isDraggingNode = false,
|
||||
bool isTurnInto = false,
|
||||
String? parentViewId,
|
||||
}) async {
|
||||
if (isDraggingNode || isTurnInto) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove the mentions that were both added and removed in the same transaction.
|
||||
// These were just moved around.
|
||||
final moved = <MentionBlockData>[];
|
||||
for (final mention in added) {
|
||||
if (removed.any((r) => r.$2 == mention.$2)) {
|
||||
moved.add(mention);
|
||||
}
|
||||
}
|
||||
|
||||
for (final mention in removed) {
|
||||
if (!context.mounted || moved.any((m) => m.$2 == mention.$2)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (mention.$2[MentionBlockKeys.type] != MentionType.date.name) {
|
||||
continue;
|
||||
}
|
||||
|
||||
_handleDeletion(context, mention);
|
||||
}
|
||||
|
||||
if (isPaste || isUndoRedo) {
|
||||
if (context.mounted) {
|
||||
context.read<ClipboardState>().startHandlingPaste(_pasteIdentifier);
|
||||
}
|
||||
|
||||
for (final mention in added) {
|
||||
if (!context.mounted || moved.any((m) => m.$2 == mention.$2)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (mention.$2[MentionBlockKeys.type] != MentionType.date.name) {
|
||||
continue;
|
||||
}
|
||||
|
||||
_handleAddition(
|
||||
context,
|
||||
viewId,
|
||||
editorState,
|
||||
mention,
|
||||
isPaste,
|
||||
isCut,
|
||||
);
|
||||
}
|
||||
|
||||
if (context.mounted) {
|
||||
context.read<ClipboardState>().endHandlingPaste(_pasteIdentifier);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _handleDeletion(
|
||||
BuildContext context,
|
||||
MentionBlockData data,
|
||||
) {
|
||||
final reminderId = data.$2[MentionBlockKeys.reminderId];
|
||||
|
||||
if (reminderId case String _ when reminderId.isNotEmpty) {
|
||||
getIt<ReminderBloc>().add(ReminderEvent.remove(reminderId: reminderId));
|
||||
}
|
||||
}
|
||||
|
||||
void _handleAddition(
|
||||
BuildContext context,
|
||||
String viewId,
|
||||
EditorState editorState,
|
||||
MentionBlockData data,
|
||||
bool isPaste,
|
||||
bool isCut,
|
||||
) {
|
||||
final dateData = _MentionDateBlockData.fromData(data.$2);
|
||||
if (dateData.dateString.isEmpty) {
|
||||
Log.error("mention date block doesn't have a valid date string");
|
||||
return;
|
||||
}
|
||||
|
||||
if (isPaste && !isCut) {
|
||||
_handlePasteFromCopy(
|
||||
context,
|
||||
viewId,
|
||||
editorState,
|
||||
data.$1,
|
||||
data.$3,
|
||||
dateData,
|
||||
);
|
||||
} else {
|
||||
_handlePasteFromCut(viewId, data.$1, dateData);
|
||||
}
|
||||
}
|
||||
|
||||
void _handlePasteFromCut(
|
||||
String viewId,
|
||||
Node node,
|
||||
_MentionDateBlockData data,
|
||||
) {
|
||||
final dateTime = DateTime.tryParse(data.dateString);
|
||||
|
||||
if (data.reminderId == null || dateTime == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
getIt<ReminderBloc>().add(
|
||||
ReminderEvent.addById(
|
||||
reminderId: data.reminderId!,
|
||||
objectId: viewId,
|
||||
scheduledAt: Int64(
|
||||
data.reminderOption
|
||||
.getNotificationDateTime(dateTime)
|
||||
.millisecondsSinceEpoch ~/
|
||||
1000,
|
||||
),
|
||||
meta: {
|
||||
ReminderMetaKeys.includeTime: data.includeTime.toString(),
|
||||
ReminderMetaKeys.blockId: node.id,
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _handlePasteFromCopy(
|
||||
BuildContext context,
|
||||
String viewId,
|
||||
EditorState editorState,
|
||||
Node node,
|
||||
int index,
|
||||
_MentionDateBlockData data,
|
||||
) async {
|
||||
final dateTime = DateTime.tryParse(data.dateString);
|
||||
|
||||
if (node.delta == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.reminderId == null || dateTime == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final reminderId = nanoid();
|
||||
getIt<ReminderBloc>().add(
|
||||
ReminderEvent.addById(
|
||||
reminderId: reminderId,
|
||||
objectId: viewId,
|
||||
scheduledAt: Int64(
|
||||
data.reminderOption
|
||||
.getNotificationDateTime(dateTime)
|
||||
.millisecondsSinceEpoch ~/
|
||||
1000,
|
||||
),
|
||||
meta: {
|
||||
ReminderMetaKeys.includeTime: data.includeTime.toString(),
|
||||
ReminderMetaKeys.blockId: node.id,
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
final newMentionAttributes = {
|
||||
MentionBlockKeys.mention: {
|
||||
MentionBlockKeys.type: MentionType.date.name,
|
||||
MentionBlockKeys.date: dateTime.toIso8601String(),
|
||||
MentionBlockKeys.reminderId: reminderId,
|
||||
MentionBlockKeys.includeTime: data.includeTime,
|
||||
MentionBlockKeys.reminderOption: data.reminderOption.name,
|
||||
},
|
||||
};
|
||||
|
||||
// The index is the index of the delta, to get the index of the mention character
|
||||
// in all the text, we need to calculate it based on the deltas before the current delta.
|
||||
int mentionIndex = 0;
|
||||
for (final (i, delta) in node.delta!.indexed) {
|
||||
if (i >= index) {
|
||||
break;
|
||||
}
|
||||
|
||||
mentionIndex += delta.length;
|
||||
}
|
||||
|
||||
// Required to prevent editing the same spot at the same time
|
||||
await Future.delayed(const Duration(milliseconds: 100));
|
||||
|
||||
final transaction = editorState.transaction
|
||||
..formatText(
|
||||
node,
|
||||
mentionIndex,
|
||||
MentionBlockKeys.mentionChar.length,
|
||||
newMentionAttributes,
|
||||
);
|
||||
|
||||
await editorState.apply(
|
||||
transaction,
|
||||
options: const ApplyOptions(recordUndo: false),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// A helper class to parse and store the mention date block data
|
||||
class _MentionDateBlockData {
|
||||
_MentionDateBlockData.fromData(Map<String, dynamic> data) {
|
||||
dateString = switch (data[MentionBlockKeys.date]) {
|
||||
final String string when DateTime.tryParse(string) != null => string,
|
||||
_ => "",
|
||||
};
|
||||
includeTime = switch (data[MentionBlockKeys.includeTime]) {
|
||||
final bool flag => flag,
|
||||
_ => false,
|
||||
};
|
||||
reminderOption = switch (data[MentionBlockKeys.reminderOption]) {
|
||||
final String name =>
|
||||
ReminderOption.values.firstWhereOrNull((o) => o.name == name) ??
|
||||
ReminderOption.none,
|
||||
_ => ReminderOption.none,
|
||||
};
|
||||
reminderId = switch (data[MentionBlockKeys.reminderId]) {
|
||||
final String id
|
||||
when id.isNotEmpty && reminderOption != ReminderOption.none =>
|
||||
id,
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
|
||||
late final String dateString;
|
||||
late final bool includeTime;
|
||||
late final String? reminderId;
|
||||
late final ReminderOption reminderOption;
|
||||
}
|
|
@ -8,7 +8,6 @@ import 'package:provider/provider.dart';
|
|||
|
||||
enum MentionType {
|
||||
page,
|
||||
reminder,
|
||||
date,
|
||||
childPage;
|
||||
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
export 'callout_node_parser.dart';
|
||||
export 'custom_image_node_parser.dart';
|
||||
export 'file_block_node_parser.dart';
|
||||
export 'link_preview_node_parser.dart';
|
||||
export 'math_equation_node_parser.dart';
|
||||
export 'simple_table_node_parser.dart';
|
||||
export 'toggle_list_node_parser.dart';
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
|
||||
class FileBlockNodeParser extends NodeParser {
|
||||
const FileBlockNodeParser();
|
||||
|
||||
@override
|
||||
String get id => FileBlockKeys.type;
|
||||
|
||||
@override
|
||||
String transform(Node node, DocumentMarkdownEncoder? encoder) {
|
||||
final name = node.attributes[FileBlockKeys.name];
|
||||
final url = node.attributes[FileBlockKeys.url];
|
||||
if (name == null || url == null) {
|
||||
return '';
|
||||
}
|
||||
return '[$name]($url)\n';
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';
|
||||
|
||||
class LinkPreviewNodeParser extends NodeParser {
|
||||
const LinkPreviewNodeParser();
|
||||
|
||||
@override
|
||||
String get id => LinkPreviewBlockKeys.type;
|
||||
|
||||
@override
|
||||
String transform(Node node, DocumentMarkdownEncoder? encoder) {
|
||||
final href = node.attributes[LinkPreviewBlockKeys.url];
|
||||
if (href == null) {
|
||||
return '';
|
||||
}
|
||||
return '[$href]($href)\n';
|
||||
}
|
||||
}
|
|
@ -1,6 +1,2 @@
|
|||
export 'callout_node_parser.dart';
|
||||
export 'custom_image_node_parser.dart';
|
||||
export 'markdown_code_parser.dart';
|
||||
export 'math_equation_node_parser.dart';
|
||||
export 'toggle_list_node_parser.dart';
|
||||
export 'simple_table_parser.dart';
|
||||
export 'markdown_simple_table_parser.dart';
|
||||
|
|
|
@ -57,6 +57,7 @@ export 'openai/widgets/ai_writer_block_component.dart';
|
|||
export 'openai/widgets/ask_ai_block_component.dart';
|
||||
export 'openai/widgets/ask_ai_toolbar_item.dart';
|
||||
export 'outline/outline_block_component.dart';
|
||||
export 'parsers/document_markdown_parsers.dart';
|
||||
export 'parsers/markdown_parsers.dart';
|
||||
export 'parsers/markdown_simple_table_parser.dart';
|
||||
export 'quote/quote_block_shortcuts.dart';
|
||||
|
|
|
@ -21,6 +21,7 @@ class SubPageTransactionHandler extends BlockTransactionHandler {
|
|||
@override
|
||||
Future<void> onTransaction(
|
||||
BuildContext context,
|
||||
String viewId,
|
||||
EditorState editorState,
|
||||
List<Node> added,
|
||||
List<Node> removed, {
|
||||
|
|
|
@ -25,6 +25,7 @@ abstract class EditorTransactionHandler<T> {
|
|||
|
||||
Future<void> onTransaction(
|
||||
BuildContext context,
|
||||
String viewId,
|
||||
EditorState editorState,
|
||||
List<T> added,
|
||||
List<T> removed, {
|
||||
|
|
|
@ -2,20 +2,23 @@ import 'dart:async';
|
|||
|
||||
import 'package:appflowy/plugins/document/presentation/editor_notification.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/child_page_transaction_handler.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/date_transaction_handler.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/sub_page/sub_page_transaction_handler.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/transaction_handler/editor_transaction_handler.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/transaction_handler/mention_transaction_handler.dart';
|
||||
import 'package:appflowy/shared/clipboard_state.dart';
|
||||
import 'package:appflowy/shared/feature_flags.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import 'mention_transaction_handler.dart';
|
||||
|
||||
final _transactionHandlers = <EditorTransactionHandler>[
|
||||
if (FeatureFlag.inlineSubPageMention.isOn) ...[
|
||||
SubPageTransactionHandler(),
|
||||
ChildPageTransactionHandler(),
|
||||
],
|
||||
DateTransactionHandler(),
|
||||
];
|
||||
|
||||
/// Handles delegating transactions to appropriate handlers.
|
||||
|
@ -148,10 +151,16 @@ class _EditorTransactionServiceState extends State<EditorTransactionService> {
|
|||
handler.type: handler.livesInDelta ? <MentionBlockData>[] : <Node>[],
|
||||
};
|
||||
|
||||
// based on the type of the transaction handler
|
||||
final uniqueTransactionHandlers = <String, EditorTransactionHandler>{};
|
||||
for (final handler in _transactionHandlers) {
|
||||
uniqueTransactionHandlers.putIfAbsent(handler.type, () => handler);
|
||||
}
|
||||
|
||||
for (final op in transaction.operations) {
|
||||
if (op is InsertOperation) {
|
||||
for (final n in op.nodes) {
|
||||
for (final handler in _transactionHandlers) {
|
||||
for (final handler in uniqueTransactionHandlers.values) {
|
||||
if (handler.livesInDelta) {
|
||||
added[handler.type]!
|
||||
.addAll(extractMentionsForType(n, handler.type));
|
||||
|
@ -163,7 +172,7 @@ class _EditorTransactionServiceState extends State<EditorTransactionService> {
|
|||
}
|
||||
} else if (op is DeleteOperation) {
|
||||
for (final n in op.nodes) {
|
||||
for (final handler in _transactionHandlers) {
|
||||
for (final handler in uniqueTransactionHandlers.values) {
|
||||
if (handler.livesInDelta) {
|
||||
removed[handler.type]!.addAll(
|
||||
extractMentionsForType(n, handler.type, false),
|
||||
|
@ -191,8 +200,9 @@ class _EditorTransactionServiceState extends State<EditorTransactionService> {
|
|||
|
||||
final (add, del) = diffDeltas(deltaBefore, deltaAfter);
|
||||
|
||||
bool fetchedMentions = false;
|
||||
for (final handler in _transactionHandlers) {
|
||||
if (!handler.livesInDelta) {
|
||||
if (!handler.livesInDelta || fetchedMentions) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
@ -212,6 +222,8 @@ class _EditorTransactionServiceState extends State<EditorTransactionService> {
|
|||
|
||||
removed[handler.type]!.addAll(mentionBlockDatas);
|
||||
}
|
||||
|
||||
fetchedMentions = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -226,6 +238,7 @@ class _EditorTransactionServiceState extends State<EditorTransactionService> {
|
|||
|
||||
handler.onTransaction(
|
||||
context,
|
||||
widget.viewId,
|
||||
widget.editorState,
|
||||
additions,
|
||||
removals,
|
||||
|
|
|
@ -12,12 +12,6 @@ typedef MentionBlockData = (Node, Map<String, dynamic>, int);
|
|||
|
||||
abstract class MentionTransactionHandler
|
||||
extends EditorTransactionHandler<MentionBlockData> {
|
||||
const MentionTransactionHandler({
|
||||
required this.subType,
|
||||
})
|
||||
const MentionTransactionHandler()
|
||||
: super(type: MentionBlockKeys.mention, livesInDelta: true);
|
||||
|
||||
final String subType;
|
||||
|
||||
MentionType get mentionType => MentionType.fromString(subType);
|
||||
}
|
||||
|
|
|
@ -19,12 +19,14 @@ class InlineActionsMenu extends InlineActionsMenuService {
|
|||
required this.initialResults,
|
||||
required this.style,
|
||||
this.startCharAmount = 1,
|
||||
this.cancelBySpaceHandler,
|
||||
});
|
||||
|
||||
final BuildContext context;
|
||||
final EditorState editorState;
|
||||
final InlineActionsService service;
|
||||
final List<InlineActionsResult> initialResults;
|
||||
final bool Function()? cancelBySpaceHandler;
|
||||
|
||||
@override
|
||||
final InlineActionsMenuStyle style;
|
||||
|
@ -137,6 +139,7 @@ class InlineActionsMenu extends InlineActionsMenuService {
|
|||
onSelectionUpdate: _onSelectionUpdate,
|
||||
style: style,
|
||||
startCharAmount: startCharAmount,
|
||||
cancelBySpaceHandler: cancelBySpaceHandler,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
@ -62,6 +62,7 @@ class InlineActionsHandler extends StatefulWidget {
|
|||
required this.onSelectionUpdate,
|
||||
required this.style,
|
||||
this.startCharAmount = 1,
|
||||
this.cancelBySpaceHandler,
|
||||
});
|
||||
|
||||
final InlineActionsService service;
|
||||
|
@ -72,6 +73,7 @@ class InlineActionsHandler extends StatefulWidget {
|
|||
final VoidCallback onSelectionUpdate;
|
||||
final InlineActionsMenuStyle style;
|
||||
final int startCharAmount;
|
||||
final bool Function()? cancelBySpaceHandler;
|
||||
|
||||
@override
|
||||
State<InlineActionsHandler> createState() => _InlineActionsHandlerState();
|
||||
|
@ -288,12 +290,17 @@ class _InlineActionsHandlerState extends State<InlineActionsHandler> {
|
|||
/// that the selection change occurred from the handler.
|
||||
widget.onSelectionUpdate();
|
||||
|
||||
if (event.logicalKey == LogicalKeyboardKey.space) {
|
||||
final cancelBySpaceHandler = widget.cancelBySpaceHandler;
|
||||
if (cancelBySpaceHandler != null && cancelBySpaceHandler()) {
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
}
|
||||
|
||||
// Interpolation to avoid having a getter for private variable
|
||||
_insertCharacter(event.character!);
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
|
||||
if (moveKeys.contains(event.logicalKey)) {
|
||||
} else if (moveKeys.contains(event.logicalKey)) {
|
||||
_moveSelection(event.logicalKey);
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
|
|
|
@ -197,8 +197,23 @@ class ShareBloc extends Bloc<ShareEvent, ShareState> {
|
|||
(p) => false,
|
||||
);
|
||||
|
||||
Log.info(
|
||||
'get publish info: $publishInfo for view: ${view.name}(${view.id})',
|
||||
// skip the "Record not found" error, it's because the view is not published yet
|
||||
publishInfo.fold(
|
||||
(s) {
|
||||
Log.info(
|
||||
'get publish info success: $publishInfo for view: ${view.name}(${view.id})',
|
||||
);
|
||||
},
|
||||
(f) {
|
||||
if (![
|
||||
ErrorCode.RecordNotFound,
|
||||
ErrorCode.LocalVersionNotSupport,
|
||||
].contains(f.code)) {
|
||||
Log.info(
|
||||
'get publish info failed: $f for view: ${view.name}(${view.id})',
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
String workspaceId = state.workspaceId;
|
||||
|
|
|
@ -20,6 +20,8 @@ String customDocumentToMarkdown(Document document) {
|
|||
const ToggleListNodeParser(),
|
||||
const CustomImageNodeParser(),
|
||||
const SimpleTableNodeParser(),
|
||||
const LinkPreviewNodeParser(),
|
||||
const FileBlockNodeParser(),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
|
|
@ -42,3 +42,6 @@ final appflowySharePageLinkRegex = RegExp(appflowySharePageLinkPattern);
|
|||
|
||||
const _numberedListPattern = r'^(\d+)\.';
|
||||
final numberedListRegex = RegExp(_numberedListPattern);
|
||||
|
||||
const _localPathPattern = r'^(file:\/\/|\/|\\|[a-zA-Z]:[/\\]|\.{1,2}[/\\])';
|
||||
final localPathRegex = RegExp(_localPathPattern, caseSensitive: false);
|
||||
|
|
|
@ -56,7 +56,7 @@ class AppFlowyCloudAuthService implements AuthService {
|
|||
(data) async {
|
||||
// Open the webview with oauth url
|
||||
final uri = Uri.parse(data.oauthUrl);
|
||||
final isSuccess = await afLaunchUrl(
|
||||
final isSuccess = await afLaunchUri(
|
||||
uri,
|
||||
mode: LaunchMode.externalApplication,
|
||||
webOnlyWindowName: '_self',
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:appflowy/core/helpers/url_launcher.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
||||
import 'package:archive/archive_io.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:open_filex/open_filex.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:share_plus/share_plus.dart';
|
||||
|
@ -67,7 +67,7 @@ Future<void> shareLogFiles(BuildContext? context) async {
|
|||
await zipFile.delete();
|
||||
} else {
|
||||
// open the directory
|
||||
await OpenFilex.open(zipFile.path);
|
||||
await afLaunchUri(zipFile.uri);
|
||||
}
|
||||
} catch (e) {
|
||||
if (context != null && context.mounted) {
|
||||
|
|
|
@ -86,7 +86,7 @@ class HomeSettingBloc extends Bloc<HomeSettingEvent, HomeSettingState> {
|
|||
},
|
||||
editPanelResized: (_EditPanelResized e) {
|
||||
final newPosition =
|
||||
(e.offset + state.resizeStart).clamp(-50, 200).toDouble();
|
||||
(state.resizeStart + e.offset).clamp(0, 200).toDouble();
|
||||
if (state.resizeOffset != newPosition) {
|
||||
emit(state.copyWith(resizeOffset: newPosition));
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ import 'package:appflowy_backend/protobuf/flowy-ai/entities.pb.dart';
|
|||
import 'package:bloc/bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:url_launcher/url_launcher.dart' show launchUrl;
|
||||
|
||||
part 'plugin_state_bloc.freezed.dart';
|
||||
|
||||
class PluginStateBloc extends Bloc<PluginStateEvent, PluginStateState> {
|
||||
|
@ -91,7 +92,7 @@ class PluginStateBloc extends Bloc<PluginStateEvent, PluginStateState> {
|
|||
final result = await AIEventGetModelStorageDirectory().send();
|
||||
result.fold(
|
||||
(data) {
|
||||
afLaunchUrl(Uri.file(data.filePath));
|
||||
afLaunchUri(Uri.file(data.filePath));
|
||||
},
|
||||
(err) => Log.error(err.toString()),
|
||||
);
|
||||
|
|
|
@ -12,6 +12,7 @@ import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
|
|||
import 'package:appflowy_backend/protobuf/flowy-user/workspace.pb.dart';
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'sidebar_plan_bloc.freezed.dart';
|
||||
|
||||
class SidebarPlanBloc extends Bloc<SidebarPlanEvent, SidebarPlanState> {
|
||||
|
@ -113,7 +114,8 @@ class SidebarPlanBloc extends Bloc<SidebarPlanEvent, SidebarPlanState> {
|
|||
} else if (error.code == ErrorCode.SingleUploadLimitExceeded) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
tierIndicator: const SidebarToastTierIndicator.singleFileLimitHit(),
|
||||
tierIndicator:
|
||||
const SidebarToastTierIndicator.singleFileLimitHit(),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
|
@ -184,13 +186,10 @@ class SidebarPlanBloc extends Bloc<SidebarPlanEvent, SidebarPlanState> {
|
|||
if (state.workspaceId != null) {
|
||||
final payload = UserWorkspaceIdPB(workspaceId: state.workspaceId!);
|
||||
UserEventGetWorkspaceUsage(payload).send().then((result) {
|
||||
result.fold(
|
||||
result.onSuccess(
|
||||
(usage) {
|
||||
add(SidebarPlanEvent.updateWorkspaceUsage(usage));
|
||||
},
|
||||
(error) {
|
||||
Log.error("Failed to get workspace usage, error: $error");
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
@ -231,7 +230,8 @@ class SidebarPlanState with _$SidebarPlanState {
|
|||
@freezed
|
||||
class SidebarToastTierIndicator with _$SidebarToastTierIndicator {
|
||||
const factory SidebarToastTierIndicator.storageLimitHit() = _StorageLimitHit;
|
||||
const factory SidebarToastTierIndicator.singleFileLimitHit() = _SingleFileLimitHit;
|
||||
const factory SidebarToastTierIndicator.singleFileLimitHit() =
|
||||
_SingleFileLimitHit;
|
||||
const factory SidebarToastTierIndicator.aiMaxiLimitHit() = _aiMaxLimitHit;
|
||||
const factory SidebarToastTierIndicator.loading() = _Loading;
|
||||
}
|
||||
|
|
|
@ -329,14 +329,15 @@ class SpaceBloc extends Bloc<SpaceEvent, SpaceState> {
|
|||
final (spaces, _, _) = await _getSpaces();
|
||||
final currentSpace = await _getLastOpenedSpace(spaces);
|
||||
|
||||
Log.info(
|
||||
'receive space update, current space: ${currentSpace?.name}(${currentSpace?.id})',
|
||||
);
|
||||
|
||||
for (var i = 0; i < spaces.length; i++) {
|
||||
Log.info(
|
||||
'did receive space update[$i]: ${spaces[i].name}(${spaces[i].id})',
|
||||
'receive space update[$i]: ${spaces[i].name}(${spaces[i].id})',
|
||||
);
|
||||
}
|
||||
Log.info(
|
||||
'did receive space update, current space: ${currentSpace?.name}(${currentSpace?.id})',
|
||||
);
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
|
|
|
@ -14,10 +14,11 @@ class HomeLayout {
|
|||
HomeLayout(BuildContext context) {
|
||||
final homeSetting = context.read<HomeSettingBloc>().state;
|
||||
showEditPanel = homeSetting.panelContext != null;
|
||||
menuWidth = Sizes.sideBarWidth;
|
||||
menuWidth += homeSetting.resizeOffset;
|
||||
|
||||
menuWidth = max(menuWidth, HomeSizes.minimumSidebarWidth);
|
||||
menuWidth = max(
|
||||
HomeSizes.minimumSidebarWidth + homeSetting.resizeOffset,
|
||||
HomeSizes.minimumSidebarWidth,
|
||||
);
|
||||
|
||||
final screenWidthPx = context.widthPx;
|
||||
context
|
||||
|
|
|
@ -1,8 +1,5 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/core/helpers/url_launcher.dart';
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
|
@ -28,6 +25,8 @@ import 'package:flowy_infra/file_picker/file_picker_service.dart';
|
|||
import 'package:flowy_infra/theme_extension.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/hover.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:fluttertoast/fluttertoast.dart';
|
||||
|
||||
|
@ -450,7 +449,7 @@ class _DataPathActions extends StatelessWidget {
|
|||
label:
|
||||
LocaleKeys.settings_manageDataPage_dataStorage_actions_open.tr(),
|
||||
icon: const FlowySvg(FlowySvgs.folder_m, size: Size.square(20)),
|
||||
onPressed: () => afLaunchUrl(Uri.file(currentPath)),
|
||||
onPressed: () => afLaunchUri(Uri.file(currentPath)),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
|
|
@ -1,14 +1,13 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/core/helpers/url_launcher.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/theme_upload/theme_upload_view.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/theme_extension.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flowy_infra_ui/widget/buttons/secondary_button.dart';
|
||||
import 'package:flowy_infra_ui/widget/error_page.dart';
|
||||
import 'package:flowy_infra/theme_extension.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ThemeUploadLearnMoreButton extends StatelessWidget {
|
||||
const ThemeUploadLearnMoreButton({super.key});
|
||||
|
@ -32,7 +31,7 @@ class ThemeUploadLearnMoreButton extends StatelessWidget {
|
|||
),
|
||||
onPressed: () async {
|
||||
final uri = Uri.parse(learnMoreURL);
|
||||
await afLaunchUrl(
|
||||
await afLaunchUri(
|
||||
uri,
|
||||
context: context,
|
||||
onFailure: (_) async {
|
||||
|
|
|
@ -220,12 +220,12 @@ class InteractiveImageToolbar extends StatelessWidget {
|
|||
Future<void> _locateOrDownloadImage(BuildContext context) async {
|
||||
if (currentImage.isLocal) {
|
||||
/// If the image type is local, we simply open the image
|
||||
await afLaunchUrl(Uri.file(currentImage.url));
|
||||
await afLaunchUri(Uri.file(currentImage.url));
|
||||
} else if (currentImage.isNotInternal) {
|
||||
// In case of eg. Unsplash images (images without extension type in URL),
|
||||
// we don't know their mimetype. In the future we can write a parser
|
||||
// using the Mime package and read the image to get the proper extension.
|
||||
await afLaunchUrl(Uri.parse(currentImage.url));
|
||||
await afLaunchUri(Uri.parse(currentImage.url));
|
||||
} else {
|
||||
if (userProfile == null) {
|
||||
return showSnapBar(
|
||||
|
|
|
@ -57,8 +57,6 @@ class Sizes {
|
|||
static double get hit => 40 * hitScale;
|
||||
|
||||
static double get iconMed => 20;
|
||||
|
||||
static double get sideBarWidth => 250 * hitScale;
|
||||
}
|
||||
|
||||
class Corners {
|
||||
|
|
|
@ -13,8 +13,8 @@ class FlowyIconTextButton extends StatelessWidget {
|
|||
final VoidCallback? onSecondaryTap;
|
||||
final void Function(bool)? onHover;
|
||||
final EdgeInsets? margin;
|
||||
final Widget Function(bool onHover)? leftIconBuilder;
|
||||
final Widget Function(bool onHover)? rightIconBuilder;
|
||||
final Widget? Function(bool onHover)? leftIconBuilder;
|
||||
final Widget? Function(bool onHover)? rightIconBuilder;
|
||||
final Color? hoverColor;
|
||||
final bool isSelected;
|
||||
final BorderRadius? radius;
|
||||
|
@ -29,6 +29,7 @@ class FlowyIconTextButton extends StatelessWidget {
|
|||
final double iconPadding;
|
||||
final bool expand;
|
||||
final Color? borderColor;
|
||||
final bool resetHoverOnRebuild;
|
||||
|
||||
const FlowyIconTextButton({
|
||||
super.key,
|
||||
|
@ -53,6 +54,7 @@ class FlowyIconTextButton extends StatelessWidget {
|
|||
this.iconPadding = 6,
|
||||
this.expand = false,
|
||||
this.borderColor,
|
||||
this.resetHoverOnRebuild = true,
|
||||
});
|
||||
|
||||
@override
|
||||
|
@ -64,6 +66,7 @@ class FlowyIconTextButton extends StatelessWidget {
|
|||
onTap: disable ? null : onTap,
|
||||
onSecondaryTap: disable ? null : onSecondaryTap,
|
||||
child: FlowyHover(
|
||||
resetHoverOnRebuild: resetHoverOnRebuild,
|
||||
cursor:
|
||||
disable ? SystemMouseCursors.forbidden : SystemMouseCursors.click,
|
||||
style: HoverStyle(
|
||||
|
@ -81,11 +84,12 @@ class FlowyIconTextButton extends StatelessWidget {
|
|||
Widget _render(BuildContext context, bool onHover) {
|
||||
final List<Widget> children = [];
|
||||
|
||||
if (leftIconBuilder != null) {
|
||||
final Widget? leftIcon = leftIconBuilder?.call(onHover);
|
||||
if (leftIcon != null) {
|
||||
children.add(
|
||||
SizedBox.fromSize(
|
||||
size: leftIconSize,
|
||||
child: leftIconBuilder!(onHover),
|
||||
child: leftIcon,
|
||||
),
|
||||
);
|
||||
children.add(HSpace(iconPadding));
|
||||
|
@ -97,10 +101,11 @@ class FlowyIconTextButton extends StatelessWidget {
|
|||
children.add(textBuilder(onHover));
|
||||
}
|
||||
|
||||
if (rightIconBuilder != null) {
|
||||
final Widget? rightIcon = rightIconBuilder?.call(onHover);
|
||||
if (rightIcon != null) {
|
||||
children.add(HSpace(iconPadding));
|
||||
// No need to define the size of rightIcon. Just use its intrinsic width
|
||||
children.add(rightIconBuilder!(onHover));
|
||||
children.add(rightIcon);
|
||||
}
|
||||
|
||||
Widget child = Row(
|
||||
|
|
|
@ -61,8 +61,8 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
path: "."
|
||||
ref: ac3b090
|
||||
resolved-ref: ac3b0906ea2e7f2e7e3c2c7852e9ec3529e07512
|
||||
ref: "157ded3"
|
||||
resolved-ref: "157ded3cd321b9a54d011c0cc27e270ded35d3aa"
|
||||
url: "https://github.com/AppFlowy-IO/appflowy-editor.git"
|
||||
source: git
|
||||
version: "4.0.0"
|
||||
|
|
|
@ -4,7 +4,7 @@ description: Bring projects, wikis, and teams together with AI. AppFlowy is an
|
|||
your data. The best open source alternative to Notion.
|
||||
publish_to: "none"
|
||||
|
||||
version: 0.7.6
|
||||
version: 0.7.7
|
||||
|
||||
environment:
|
||||
flutter: ">=3.22.0"
|
||||
|
@ -173,7 +173,7 @@ dependency_overrides:
|
|||
appflowy_editor:
|
||||
git:
|
||||
url: https://github.com/AppFlowy-IO/appflowy-editor.git
|
||||
ref: "ac3b090"
|
||||
ref: "157ded3"
|
||||
|
||||
appflowy_editor_plugins:
|
||||
git:
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
|
||||
import 'package:appflowy/shared/markdown_to_document.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
group('export markdown to document', () {
|
||||
test('file block', () async {
|
||||
final document = Document.blank()
|
||||
..insert(
|
||||
[0],
|
||||
[
|
||||
fileNode(
|
||||
name: 'file.txt',
|
||||
url: 'https://file.com',
|
||||
),
|
||||
],
|
||||
);
|
||||
final markdown = customDocumentToMarkdown(document);
|
||||
expect(markdown, '[file.txt](https://file.com)\n');
|
||||
});
|
||||
|
||||
test('link preview', () {
|
||||
final document = Document.blank()
|
||||
..insert(
|
||||
[0],
|
||||
[linkPreviewNode(url: 'https://www.link_preview.com')],
|
||||
);
|
||||
final markdown = customDocumentToMarkdown(document);
|
||||
expect(
|
||||
markdown,
|
||||
'[https://www.link_preview.com](https://www.link_preview.com)\n',
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
import 'package:appflowy/shared/patterns/common_patterns.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
group('url launcher unit test', () {
|
||||
test('launch local uri', () async {
|
||||
const localUris = [
|
||||
'file://path/to/file.txt',
|
||||
'/path/to/file.txt',
|
||||
'C:\\path\\to\\file.txt',
|
||||
'../path/to/file.txt',
|
||||
];
|
||||
for (final uri in localUris) {
|
||||
final result = localPathRegex.hasMatch(uri);
|
||||
expect(result, true);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
|
@ -2896,5 +2896,12 @@
|
|||
"favoriteDisabledHint": "Cannot favorite this view",
|
||||
"pinTab": "Pin",
|
||||
"unpinTab": "Unpin"
|
||||
},
|
||||
"openFileMessage": {
|
||||
"success": "File opened successfully",
|
||||
"fileNotFound": "File not found",
|
||||
"noAppToOpenFile": "No app to open this file",
|
||||
"permissionDenied": "No permission to open this file",
|
||||
"unknownError": "File open failed"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -903,18 +903,21 @@ impl DatabaseCollabService for WorkspaceDatabaseCollabServiceImpl {
|
|||
encoded_collab_by_id.insert(k, v);
|
||||
}
|
||||
|
||||
// 2. Fetch remaining collabs from remote
|
||||
let remote_collabs = self
|
||||
.batch_get_encode_collab(object_ids, collab_type)
|
||||
.await?;
|
||||
if !object_ids.is_empty() {
|
||||
// 2. Fetch remaining collabs from remote
|
||||
let remote_collabs = self
|
||||
.batch_get_encode_collab(object_ids, collab_type)
|
||||
.await?;
|
||||
|
||||
trace!(
|
||||
"[Database]: load {} database row from remote",
|
||||
remote_collabs.len()
|
||||
);
|
||||
for (k, v) in remote_collabs {
|
||||
encoded_collab_by_id.insert(k, v);
|
||||
trace!(
|
||||
"[Database]: load {} database row from remote",
|
||||
remote_collabs.len()
|
||||
);
|
||||
for (k, v) in remote_collabs {
|
||||
encoded_collab_by_id.insert(k, v);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(encoded_collab_by_id)
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue