Compare commits

...
32 Commits
Author SHA1 Message Date
Mathias Mogensen 966547faa0 fix: reminder launch review (#3716) 2023-10-17 14:48:58 +08:00
Muhammad Saad Tanveer 3647af44c9 fix: Shortcut settings Reset to default is missing bottom margin #3710 (#3715) 2023-10-17 05:47:07 +02:00
Lucas.Xu 25b4a647b0 fix: unable to export image to markdown (#3709) 2023-10-16 15:56:30 +08:00
Lucas.Xu c6e3c1fd7c feat: customizing the background of a block with theme color (#3691) 2023-10-16 14:11:05 +08:00
ルビーDog 4fd35d0de4 feat: improve translations ja_JP (#3702) (#3705) 2023-10-16 10:56:06 +08:00
Lucas.Xu 7406c5e6a5 feat: add toggle list and callout node parser (#3700) 2023-10-16 10:09:33 +08:00
CrazyRikin aa70885b21 feat: added hindi translation (#3575) 2023-10-15 22:13:14 +08:00
Yijing Huang 8699c9c125 fix: lemonade theme issues (#3686) 2023-10-15 22:12:25 +08:00
Pedro Coelho bc758e0b06 chore: update translations pt-PT (#3687) 2023-10-15 22:12:01 +08:00
Jakob Gerhardt e38980ccc3 chore: update translations de-DE (#3693) 2023-10-15 22:11:28 +08:00
Verdy Bangkit 8dd180046f chore: update translations id_ID and ru_RU (#3701) 2023-10-15 22:10:41 +08:00
Lucas.Xu 0cd2726752 chore: disable toolbar in code block (#3682) 2023-10-15 22:01:17 +08:00
Lucas.Xu 11daa15b89 feat: auto pop up the menu after inserting image block (#3698) 2023-10-15 13:04:24 +08:00
Lucas.Xu 8436bc6144 fix: the disk space of CI machines always runs out (#3689) 2023-10-14 20:37:44 +08:00
Ekaksh JanwejaandLucas.Xu 59851f46b7 feat: edit the field by clicking on the drag icon (#3656)
* feat: edit the field by clicking on the drag icon

* Added localization

* Added .tr() function

* fix: BlockActionButton only show when _isFieldHover is true.

* fix: BlockActionButton only show when _isFieldHover is true.

* fix: Worked on requested changes

* fix: Fixed Location of Field Popover

* fixed pubspec.lock

* Updated pubspec.lock

---------

Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io>
2023-10-13 23:47:49 +08:00
Sarthak Kimtani 62f1d6eb82 fix: 3620 unchecked checkbox color (#3685) 2023-10-13 21:06:13 +08:00
Nathan.fooo ec80f72c8f feat: af cloud database (#3684)
* chore: forece user to logout when token is invalid

* chore: update client api rev
2023-10-12 20:25:00 +08:00
Richard Shiue b2c80981cc fix: build app (#3681) 2023-10-11 22:46:32 -07:00
Yijing Huang 8fa425a76c chore: bump version 0.3.6 (#3680) 2023-10-12 13:14:37 +08:00
Mathias Mogensen ebe112581d feat: toggle notifications on/off (#3672) 2023-10-12 12:54:08 +08:00
Mathias Mogensen bc8f35d7db feat: reminder improvements (#3658) 2023-10-12 10:19:36 +08:00
Nathan.fooo 058eeec932 feat: implement file storage using appflowy cloud (#3675)
* feat: implement file storage using appflowy cloud

* chore: clippy
2023-10-12 09:54:45 +08:00
Lucas.Xu 8e10cba8e5 feat: support toggling the markdown style by shortcuts (#3674) 2023-10-11 23:12:23 +08:00
Yijing Huang 7a24f6b6f0 feat: add lemonade theme color (#3666) 2023-10-11 22:01:05 +08:00
Richard Shiue 4f40945458 fix: integration tests (#3673)
* fix: fix integration tests

* fix: initiate isScreenSmall properly
2023-10-11 20:36:10 +08:00
Nathan.fooo e59870e421 refactor: rename flowy-test to event-integration (#3667) 2023-10-11 20:02:09 +08:00
Tran Nguyen Sinh cf521fe20b chore: update translations for Vietnamese (#3632) 2023-10-11 17:15:59 +07:00
Mathias Mogensen f0d47ee50c fix: esc to close date picker dialog (#3648) 2023-10-11 05:49:32 +02:00
Lucas.Xu 2a5d393c9f fix: missing dependency (libnotify) in appimage (#3665) 2023-10-11 09:46:59 +08:00
Shreesh Nautiyal 98e0b832ec feat: 1674 Auto-hide sidebar when re-sizing the window (#3659)
* feat: 1674 Auto-hide sidebar when re-sizing the window

* chore: removed unnecessary print statements

* chore: updated to a more approprite name for 'screenSmall' event
2023-10-10 19:26:11 +02:00
Nathan.fooo 77e7cbe9ec chore: sync document (#3653)
* chore: sync document

* chore: update collab rev

* chore: update collab rev
2023-10-10 19:05:55 +08:00
Dhruv Sharma acd53eb640 fix: correct svg for row actions (#3664)
Closes: #3450
2023-10-10 17:30:44 +08:00
278 changed files with 3908 additions and 968 deletions

No files matched your search

+9
View File
@@ -49,6 +49,15 @@ jobs:
runs-on: ${{ matrix.os }}
steps:
# the following step is required to avoid running out of space
- name: Maximize build space
if: matrix.os == 'ubuntu-latest'
run: |
sudo rm -rf /usr/share/dotnet
sudo rm -rf /opt/ghc
sudo rm -rf "/usr/local/share/boost"
sudo rm -rf "$AGENT_TOOLSDIRECTORY"
- name: Checkout source code
uses: actions/checkout@v2
+9
View File
@@ -45,6 +45,15 @@ jobs:
runs-on: ${{ matrix.os }}
steps:
# the following step is required to avoid running out of space
- name: Maximize build space
if: matrix.os == 'ubuntu-latest'
run: |
sudo rm -rf /usr/share/dotnet
sudo rm -rf /opt/ghc
sudo rm -rf /usr/local/share/boost
df -h
- name: Checkout source code
uses: actions/checkout@v2
+2 -2
View File
@@ -60,8 +60,8 @@ jobs:
echo SUPABASE_ANON_KEY=${{ secrets.SUPABASE_ANON_KEY }} >> .env.ci
echo SUPABASE_JWT_SECRET=${{ secrets.SUPABASE_JWT_SECRET }} >> .env.ci
- name: Create .env file in flowy-test
working-directory: frontend/rust-lib/flowy-test
- name: Create .env file in event-integration
working-directory: frontend/rust-lib/event-integration
run: |
touch .env.ci
echo SUPABASE_URL=${{ secrets.SUPABASE_URL }} >> .env.ci
+15
View File
@@ -1,5 +1,20 @@
# Release Notes
## Version 0.3.6 - 10/16/2023
### New Features
- Support setting Markdown styles through keyboard shortcuts.
- Added Ukrainian language.
- Support auto-hiding sidebar feature, ensuring a streamlined view even when resizing to a smaller window.
- Support toggling the notifitcation on/off.
- Added Lemonade theme.
### Bug fixes
- Improve Vietnamese translations.
- Improve reminder feature.
- Fix some UI issues.
## Version 0.3.5 - 10/09/2023
### New Features
+1 -1
View File
@@ -25,7 +25,7 @@ CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true
CARGO_MAKE_CRATE_FS_NAME = "dart_ffi"
CARGO_MAKE_CRATE_NAME = "dart-ffi"
LIB_NAME = "dart_ffi"
CURRENT_APP_VERSION = "0.3.5"
CURRENT_APP_VERSION = "0.3.6"
FLUTTER_DESKTOP_FEATURES = "dart,rev-sqlite"
PRODUCT_NAME = "AppFlowy"
# CRATE_TYPE: https://doc.rust-lang.org/reference/linkage.html
@@ -245,7 +245,7 @@ void main() {
await tester.wait(500);
// Focus on the editor
final textBlock = find.byType(TextBlockComponentWidget);
final textBlock = find.byType(ParagraphBlockComponentWidget);
await tester.tapAt(tester.getCenter(textBlock));
await tester.pumpAndSettle();
@@ -24,7 +24,7 @@ void main() {
LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
);
// and with one paragraph block
expect(find.byType(TextBlockComponentWidget), findsOneWidget);
expect(find.byType(ParagraphBlockComponentWidget), findsOneWidget);
});
testWidgets('delete the readme page and restore it', (tester) async {
@@ -1,4 +1,4 @@
import 'package:appflowy/workspace/application/appearance.dart';
import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
@@ -9,6 +9,7 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/image/uplo
import 'package:appflowy_backend/protobuf/flowy-folder2/protobuf.dart';
import 'package:appflowy_editor/appflowy_editor.dart' hide UploadImageMenu;
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
@@ -41,8 +42,13 @@ void main() {
await tester.editor.tapSlashMenuItemWithName('Image');
expect(find.byType(CustomImageBlockComponent), findsOneWidget);
expect(find.byType(ImagePlaceholder), findsOneWidget);
await tester.tapButton(find.byType(ImagePlaceholder));
expect(
find.descendant(
of: find.byType(ImagePlaceholder),
matching: find.byType(AppFlowyPopover),
),
findsOneWidget,
);
expect(find.byType(UploadImageMenu), findsOneWidget);
final image = await rootBundle.load('assets/test/images/sample.jpeg');
@@ -84,8 +90,13 @@ void main() {
await tester.editor.tapSlashMenuItemWithName('Image');
expect(find.byType(CustomImageBlockComponent), findsOneWidget);
expect(find.byType(ImagePlaceholder), findsOneWidget);
await tester.tapButton(find.byType(ImagePlaceholder));
expect(
find.descendant(
of: find.byType(ImagePlaceholder),
matching: find.byType(AppFlowyPopover),
),
findsOneWidget,
);
expect(find.byType(UploadImageMenu), findsOneWidget);
await tester.tapButtonWithName(
@@ -133,8 +144,13 @@ void main() {
await tester.editor.tapSlashMenuItemWithName('Image');
expect(find.byType(CustomImageBlockComponent), findsOneWidget);
expect(find.byType(ImagePlaceholder), findsOneWidget);
await tester.tapButton(find.byType(ImagePlaceholder));
expect(
find.descendant(
of: find.byType(ImagePlaceholder),
matching: find.byType(AppFlowyPopover),
),
findsOneWidget,
);
expect(find.byType(UploadImageMenu), findsOneWidget);
await tester.tapButtonWithName(
@@ -4,6 +4,7 @@ import 'package:appflowy/plugins/document/presentation/share/share_button.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:path/path.dart' as p;
import 'util/mock/mock_file_picker.dart';
import 'util/util.dart';
@@ -78,7 +79,7 @@ void main() {
});
}
const expectedMarkdown = r'''
const expectedMarkdown = '''
# Welcome to AppFlowy!
## Here are the basics
- [ ] Click anywhere and just start typing.
@@ -105,6 +106,15 @@ fn main() {
## Have a question❓
> Click `?` at the bottom right for help and support.
> 🥰
>
> Like AppFlowy? Follow us:
> [GitHub](https://github.com/AppFlowy-IO/AppFlowy)
> [Twitter](https://twitter.com/appflowy): @appflowy
> [Newsletter](https://blog-appflowy.ghost.io/)
>
''';
@@ -30,7 +30,7 @@ void main() {
LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
);
// and with one paragraph block
expect(find.byType(TextBlockComponentWidget), findsOneWidget);
expect(find.byType(ParagraphBlockComponentWidget), findsOneWidget);
});
testWidgets('create a new document, grid, board and calendar',
@@ -55,7 +55,7 @@ void main() {
switch (layout) {
case ViewLayoutPB.Document:
// and with one paragraph block
expect(find.byType(TextBlockComponentWidget), findsOneWidget);
expect(find.byType(ParagraphBlockComponentWidget), findsOneWidget);
break;
case ViewLayoutPB.Grid:
expect(find.byType(GridPage), findsOneWidget);
@@ -1,9 +1,12 @@
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 'package:path/path.dart' as p;
import 'util/mock/mock_file_picker.dart';
import 'util/util.dart';
@@ -11,6 +14,10 @@ void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('customize the folder path', () {
if (Platform.isWindows) {
return;
}
testWidgets('switch to B from A, then switch to A again', (tester) async {
const userA = 'UserA';
const userB = 'UserB';
@@ -1,5 +1,5 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/workspace/application/appearance.dart';
import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
import 'package:appflowy/workspace/application/settings/prelude.dart';
import 'package:appflowy/workspace/presentation/settings/settings_dialog.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/settings_menu_element.dart';
@@ -381,14 +381,10 @@ Widget? _buildHeaderIcon(GroupData customData) {
switch (customData.fieldType) {
case FieldType.Checkbox:
final group = customData.asCheckboxGroup()!;
if (group.isCheck) {
widget = const FlowySvg(
FlowySvgs.check_filled_s,
blendMode: BlendMode.dst,
);
} else {
widget = const FlowySvg(FlowySvgs.uncheck_s);
}
widget = FlowySvg(
group.isCheck ? FlowySvgs.check_filled_s : FlowySvgs.uncheck_s,
blendMode: BlendMode.dst,
);
break;
case FieldType.DateTime:
case FieldType.LastEditedTime:
@@ -230,7 +230,7 @@ class _RowMenuButtonState extends State<RowMenuButton> {
onPressed: () => widget.openMenu(),
iconPadding: const EdgeInsets.all(3),
icon: FlowySvg(
FlowySvgs.details_s,
FlowySvgs.drag_element_s,
color: Theme.of(context).colorScheme.tertiary,
),
);
@@ -39,12 +39,11 @@ class _CheckboxCellState extends State<CheckboxCardCell> {
buildWhen: (previous, current) =>
previous.isSelected != current.isSelected,
builder: (context, state) {
final icon = state.isSelected
? const FlowySvg(
FlowySvgs.check_filled_s,
blendMode: BlendMode.dst,
)
: const FlowySvg(FlowySvgs.uncheck_s);
final icon = FlowySvg(
state.isSelected ? FlowySvgs.check_filled_s : FlowySvgs.uncheck_s,
blendMode: BlendMode.dst,
);
return Align(
alignment: Alignment.centerLeft,
child: Padding(
@@ -67,7 +67,7 @@ class _PrimaryCellAccessoryState extends State<PrimaryCellAccessory>
with GridCellAccessoryState {
@override
Widget build(BuildContext context) {
return FlowyTooltip.delayed(
return FlowyTooltip(
message: LocaleKeys.tooltip_openAsPage.tr(),
child: SizedBox(
width: 26,
@@ -116,6 +116,9 @@ class CheckboxCellUncheck extends StatelessWidget {
@override
Widget build(BuildContext context) {
return const FlowySvg(FlowySvgs.uncheck_s);
return const FlowySvg(
FlowySvgs.uncheck_s,
blendMode: BlendMode.dst,
);
}
}
@@ -366,6 +366,9 @@ class EndTimeButton extends StatelessWidget {
}
}
const _maxLengthTwelveHour = 8;
const _maxLengthTwentyFourHour = 5;
class _TimeTextField extends StatefulWidget {
final bool isEndTime;
final String? timeStr;
@@ -433,6 +436,11 @@ class _TimeTextFieldState extends State<_TimeTextField> {
errorText: widget.isEndTime
? state.parseEndTimeError
: state.parseTimeError,
maxLength:
state.dateTypeOptionPB.timeFormat == TimeFormatPB.TwelveHour
? _maxLengthTwelveHour
: _maxLengthTwentyFourHour,
showCounter: false,
onSubmitted: (timeStr) {
if (widget.isEndTime) {
context
@@ -9,6 +9,7 @@ import 'package:appflowy/plugins/database_view/grid/application/row/row_detail_b
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_cell.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_editor.dart';
import 'package:appflowy/plugins/database_view/widgets/row/cells/cells.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/block_action_button.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
@@ -112,6 +113,7 @@ class RowPropertyList extends StatelessWidget {
class _PropertyCell extends StatefulWidget {
final DatabaseCellContext cellContext;
final GridCellBuilder cellBuilder;
final int index;
const _PropertyCell({
required this.cellContext,
@@ -126,6 +128,8 @@ class _PropertyCell extends StatefulWidget {
class _PropertyCellState extends State<_PropertyCell> {
final PopoverController _popoverController = PopoverController();
final PopoverController _fieldPopoverController = PopoverController();
bool _isFieldHover = false;
@override
@@ -140,7 +144,23 @@ class _PropertyCellState extends State<_PropertyCell> {
child: SizedBox(
width: 16,
height: 30,
child: _isFieldHover ? const FlowySvg(FlowySvgs.drag_element_s) : null,
child: AppFlowyPopover(
controller: _fieldPopoverController,
constraints: BoxConstraints.loose(const Size(240, 600)),
margin: EdgeInsets.zero,
triggerActions: PopoverTriggerFlags.none,
direction: PopoverDirection.bottomWithLeftAligned,
popupBuilder: (popoverContext) => buildFieldEditor(),
child: _isFieldHover
? BlockActionButton(
onTap: () => _fieldPopoverController.show(),
svg: FlowySvgs.drag_element_s,
richMessage: TextSpan(
text: LocaleKeys.grid_rowPage_fieldDragEelementTooltip.tr(),
),
)
: const SizedBox.shrink(),
),
),
);
@@ -104,6 +104,7 @@ class DatabaseShareActionListState extends State<DatabaseShareActionList> {
buildChild: (controller) {
return RoundedTextButton(
title: LocaleKeys.shareAction_buttonText.tr(),
textColor: Theme.of(context).colorScheme.onSurface,
onPressed: () => controller.show(),
);
},
@@ -1,4 +1,5 @@
import 'package:appflowy/plugins/document/application/doc_bloc.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/background_color/theme_background_color.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/custom_image_block_component.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
import 'package:appflowy/plugins/document/presentation/editor_style.dart';
@@ -7,7 +8,7 @@ import 'package:appflowy/plugins/inline_actions/handlers/inline_page_reference.d
import 'package:appflowy/plugins/inline_actions/handlers/reminder_reference.dart';
import 'package:appflowy/plugins/inline_actions/inline_actions_command.dart';
import 'package:appflowy/plugins/inline_actions/inline_actions_service.dart';
import 'package:appflowy/workspace/application/appearance.dart';
import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
import 'package:appflowy/workspace/application/settings/shortcuts/settings_shortcuts_service.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/emoji_picker/emoji_picker.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
@@ -154,7 +155,15 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
supportSlashMenuNodeWhiteList.addAll([
ToggleListBlockKeys.type,
]);
toolbarItemWhiteList.addAll([
ToggleListBlockKeys.type,
CalloutBlockKeys.type,
TableBlockKeys.type,
]);
AppFlowyRichTextKeys.supportSliced.add(AppFlowyRichTextKeys.fontFamily);
// customize the dynamic theme color
_customizeBlockComponentBackgroundColorDecorator();
}
@override
@@ -240,7 +249,7 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
final customBlockComponentBuilderMap = {
PageBlockKeys.type: PageBlockComponentBuilder(),
ParagraphBlockKeys.type: TextBlockComponentBuilder(
ParagraphBlockKeys.type: ParagraphBlockComponentBuilder(
configuration: configuration,
),
TodoListBlockKeys.type: TodoListBlockComponentBuilder(
@@ -536,4 +545,33 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
),
);
}
void _customizeBlockComponentBackgroundColorDecorator() {
blockComponentBackgroundColorDecorator = (Node node, String colorString) {
// the color string is from FlowyTint.
final tintColor = FlowyTint.values.firstWhereOrNull(
(e) => e.id == colorString,
);
if (tintColor != null) {
return tintColor.color(context);
}
final themeColor = themeBackgroundColors[colorString];
if (themeColor != null) {
return themeColor.color(context);
}
if (colorString == optionActionColorDefaultColor) {
final defaultColor = node.type == CalloutBlockKeys.type
? AFThemeExtension.of(context).calloutBGColor
: Colors.transparent;
return defaultColor;
}
if (colorString == tableCellDefaultColor) {
return AFThemeExtension.of(context).tableCellBGColor;
}
return null;
};
}
}
@@ -21,7 +21,7 @@ class BlockActionButton extends StatelessWidget {
Widget build(BuildContext context) {
return Align(
alignment: Alignment.center,
child: FlowyTooltip.delayed(
child: FlowyTooltip(
preferBelow: false,
richMessage: richMessage,
child: MouseRegion(
@@ -2,7 +2,7 @@ import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/block_action_button.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/option_action.dart';
import 'package:appflowy/workspace/application/appearance.dart';
import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
@@ -47,7 +47,7 @@ class BlockOptionButton extends StatelessWidget {
: PopoverDirection.leftWithCenterAligned,
actions: popoverActions,
onPopupBuilder: () {
keepEditorFocusNotifier.value += 1;
keepEditorFocusNotifier.increase();
blockComponentState.alwaysShowActions = true;
},
onClosed: () {
@@ -55,7 +55,7 @@ class BlockOptionButton extends StatelessWidget {
editorState.selectionType = null;
editorState.selection = null;
blockComponentState.alwaysShowActions = false;
keepEditorFocusNotifier.value -= 1;
keepEditorFocusNotifier.decrease();
});
},
onSelected: (action, controller) {
@@ -10,6 +10,8 @@ import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:styled_widget/styled_widget.dart';
const optionActionColorDefaultColor = 'appflowy_theme_default_color';
enum OptionAction {
delete,
duplicate,
@@ -251,12 +253,14 @@ class ColorOptionAction extends PopoverActionCell {
// reset to default background color
FlowyColorOption(
color: defaultColor,
name: LocaleKeys.document_plugins_optionAction_defaultColor.tr(),
i18n: LocaleKeys.document_plugins_optionAction_defaultColor.tr(),
id: optionActionColorDefaultColor,
),
...FlowyTint.values.map(
(e) => FlowyColorOption(
color: e.color(context),
name: e.tintName(AppFlowyEditorLocalizations.current),
i18n: e.tintName(AppFlowyEditorLocalizations.current),
id: e.id,
),
),
];
@@ -268,11 +272,10 @@ class ColorOptionAction extends PopoverActionCell {
color: Theme.of(context).colorScheme.onBackground,
width: 1,
),
onTap: (color, index) async {
onTap: (option, index) async {
final transaction = editorState.transaction;
final backgroundColor = color.toHex();
transaction.updateNode(node, {
blockComponentBackgroundColor: backgroundColor,
blockComponentBackgroundColor: option.id,
});
await editorState.apply(transaction);
@@ -3,7 +3,6 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/op
import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:flowy_infra_ui/widget/ignore_parent_gesture.dart';
import 'package:flutter/material.dart';
@@ -112,32 +111,6 @@ class OptionActionList extends StatelessWidget {
}
}
class BlockComponentActionButton extends StatelessWidget {
const BlockComponentActionButton({
super.key,
required this.icon,
required this.onTap,
});
final bool isHovering = false;
final Widget icon;
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
return MouseRegion(
cursor: SystemMouseCursors.grab,
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: onTap,
onTapDown: (details) {},
onTapUp: (details) {},
child: icon,
),
);
}
}
class OptionActionButton extends StatelessWidget {
const OptionActionButton({
super.key,
@@ -11,7 +11,7 @@ final alignToolbarItem = ToolbarItem(
id: 'editor.align',
group: 4,
isActive: onlyShowInTextType,
builder: (context, editorState, highlightColor) {
builder: (context, editorState, highlightColor, _) {
final selection = editorState.selection!;
final nodes = editorState.getNodesInSelection(selection);
@@ -89,11 +89,11 @@ class _AlignmentButtonsState extends State<_AlignmentButtons> {
borderRadius: const BorderRadius.all(Radius.circular(4)),
),
popupBuilder: (_) {
keepEditorFocusNotifier.value += 1;
keepEditorFocusNotifier.increase();
return _AlignButtons(onAlignChanged: widget.onAlignChanged);
},
onClose: () {
keepEditorFocusNotifier.value -= 1;
keepEditorFocusNotifier.decrease();
},
child: widget.child,
);
@@ -156,7 +156,7 @@ class _AlignButton extends StatelessWidget {
cursor: SystemMouseCursors.click,
child: GestureDetector(
onTap: onTap,
child: FlowyTooltip.delayed(
child: FlowyTooltip(
message: tooltips,
child: FlowySvg(
icon,
@@ -0,0 +1,15 @@
import 'package:flowy_infra/theme_extension.dart';
// DON'T MODIFY THIS KEY BECAUSE IT'S SAVED IN THE DATABASE!
// Used for the block component background color
const themeBackgroundColors = {
'appflowy_them_color_tint1': FlowyTint.tint1,
'appflowy_them_color_tint2': FlowyTint.tint2,
'appflowy_them_color_tint3': FlowyTint.tint3,
'appflowy_them_color_tint4': FlowyTint.tint4,
'appflowy_them_color_tint5': FlowyTint.tint5,
'appflowy_them_color_tint6': FlowyTint.tint6,
'appflowy_them_color_tint7': FlowyTint.tint7,
'appflowy_them_color_tint8': FlowyTint.tint8,
'appflowy_them_color_tint9': FlowyTint.tint9,
};
@@ -1,4 +1,5 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/insert_page_command.dart';
import 'package:appflowy/workspace/application/view/view_ext.dart';
import 'package:appflowy/workspace/application/view/view_service.dart';
@@ -6,13 +7,12 @@ import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/style_widget/button.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flowy_infra_ui/widget/error_page.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:easy_localization/easy_localization.dart';
void showLinkToPageMenu(
OverlayState container,
@@ -27,13 +27,13 @@ void showLinkToPageMenu(
final top = alignment == Alignment.bottomLeft ? offset.dy : null;
final bottom = alignment == Alignment.topLeft ? offset.dy : null;
keepEditorFocusNotifier.value += 1;
keepEditorFocusNotifier.increase();
late OverlayEntry linkToPageMenuEntry;
linkToPageMenuEntry = FullScreenOverlayEntry(
top: top,
bottom: bottom,
left: offset.dx,
dismissCallback: () => keepEditorFocusNotifier.value -= 1,
dismissCallback: () => keepEditorFocusNotifier.decrease(),
builder: (context) => Material(
color: Colors.transparent,
child: LinkToPageMenu(
@@ -225,11 +225,9 @@ class _LinkToPageMenuState extends State<LinkToPageMenu> {
crossAxisAlignment: CrossAxisAlignment.stretch,
children: children,
);
} else {
return const Center(
child: CircularProgressIndicator(),
);
}
return const Center(child: CircularProgressIndicator());
},
future: items,
);
@@ -88,8 +88,7 @@ class CalloutBlockComponentBuilder extends BlockComponentBuilder {
bool validate(Node node) =>
node.delta != null &&
node.children.isEmpty &&
node.attributes[CalloutBlockKeys.icon] is String &&
node.attributes[CalloutBlockKeys.backgroundColor] is String;
node.attributes[CalloutBlockKeys.icon] is String;
}
// the main widget for rendering the callout block
@@ -117,7 +116,8 @@ class _CalloutBlockComponentWidgetState
DefaultSelectableMixin,
BlockComponentConfigurable,
BlockComponentTextDirectionMixin,
BlockComponentAlignMixin {
BlockComponentAlignMixin,
BlockComponentBackgroundColorMixin {
// the key used to forward focus to the richtext child
@override
final forwardKey = GlobalKey(debugLabel: 'flowy_rich_text');
@@ -137,11 +137,13 @@ class _CalloutBlockComponentWidgetState
@override
Node get node => widget.node;
// get the background color of the note block from the node's attributes
@override
Color get backgroundColor {
final colorString =
node.attributes[CalloutBlockKeys.backgroundColor] as String;
return colorString.tryToColor() ?? Colors.transparent;
final color = super.backgroundColor;
if (color == Colors.transparent) {
return AFThemeExtension.of(context).calloutBGColor;
}
return color;
}
// get the emoji of the note block from the node's attributes or default to '📌'
@@ -4,6 +4,7 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/base/strin
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart' hide TextDirection;
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:highlight/highlight.dart' as highlight;
@@ -190,7 +191,7 @@ class _CodeBlockComponentWidgetState extends State<CodeBlockComponentWidget>
Widget child = Container(
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(8.0)),
color: Colors.grey.withOpacity(0.1),
color: AFThemeExtension.of(context).calloutBGColor,
),
width: MediaQuery.of(context).size.width,
child: Column(
@@ -24,7 +24,7 @@ class FindAndReplaceMenuWidget extends StatefulWidget {
class _FindAndReplaceMenuWidgetState extends State<FindAndReplaceMenuWidget> {
bool showReplaceMenu = false;
late SearchServiceV2 searchService = SearchServiceV2(
late SearchServiceV3 searchService = SearchServiceV3(
editorState: widget.editorState,
);
@@ -72,7 +72,7 @@ class FindMenu extends StatefulWidget {
final EditorState editorState;
final VoidCallback onDismiss;
final SearchServiceV2 searchService;
final SearchServiceV3 searchService;
final void Function(bool value) onShowReplace;
@override
@@ -93,7 +93,7 @@ class _FindMenuState extends State<FindMenu> {
void initState() {
super.initState();
widget.searchService.matchedPositions.addListener(_setState);
widget.searchService.matchWrappers.addListener(_setState);
widget.searchService.currentSelectedIndex.addListener(_setState);
findTextEditingController.addListener(_searchPattern);
@@ -105,7 +105,7 @@ class _FindMenuState extends State<FindMenu> {
@override
void dispose() {
widget.searchService.matchedPositions.removeListener(_setState);
widget.searchService.matchWrappers.removeListener(_setState);
widget.searchService.currentSelectedIndex.removeListener(_setState);
widget.searchService.dispose();
findTextEditingController.removeListener(_searchPattern);
@@ -117,7 +117,7 @@ class _FindMenuState extends State<FindMenu> {
Widget build(BuildContext context) {
// the selectedIndex from searchService is 0-based
final selectedIndex = widget.searchService.selectedIndex + 1;
final matches = widget.searchService.matchedPositions.value;
final matches = widget.searchService.matchWrappers.value;
return Row(
children: [
const HSpace(4.0),
@@ -232,7 +232,7 @@ class ReplaceMenu extends StatefulWidget {
/// The localizations of the find and replace menu
final FindReplaceLocalizations? localizations;
final SearchServiceV2 searchService;
final SearchServiceV3 searchService;
@override
State<ReplaceMenu> createState() => _ReplaceMenuState();
@@ -9,7 +9,7 @@ final customizeFontToolbarItem = ToolbarItem(
id: 'editor.font',
group: 4,
isActive: onlyShowInTextType,
builder: (context, editorState, highlightColor) {
builder: (context, editorState, highlightColor, _) {
final selection = editorState.selection!;
final popoverController = PopoverController();
return MouseRegion(
@@ -18,8 +18,8 @@ final customizeFontToolbarItem = ToolbarItem(
currentFontFamily: '',
offset: const Offset(0, 12),
popoverController: popoverController,
onOpen: () => keepEditorFocusNotifier.value += 1,
onClose: () => keepEditorFocusNotifier.value -= 1,
onOpen: () => keepEditorFocusNotifier.increase(),
onClose: () => keepEditorFocusNotifier.decrease(),
showResetButton: true,
onFontFamilyChanged: (fontFamily) async {
popoverController.close();
@@ -3,6 +3,8 @@ import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
const kImagePlaceholderKey = 'imagePlaceholderKey';
typedef CustomImageBlockComponentMenuBuilder = Widget Function(
Node node,
CustomImageBlockComponentState state,
@@ -93,8 +95,10 @@ class CustomImageBlockComponentState extends State<CustomImageBlockComponent>
MediaQuery.of(context).size.width;
final height = attributes[ImageBlockKeys.height]?.toDouble();
final imagePlaceholderKey = node.extraInfos?[kImagePlaceholderKey];
Widget child = src.isEmpty
? ImagePlaceholder(
key: imagePlaceholderKey is GlobalKey ? imagePlaceholderKey : null,
node: node,
)
: ResizableImage(
@@ -28,10 +28,10 @@ class ImagePlaceholder extends StatefulWidget {
final Node node;
@override
State<ImagePlaceholder> createState() => _ImagePlaceholderState();
State<ImagePlaceholder> createState() => ImagePlaceholderState();
}
class _ImagePlaceholderState extends State<ImagePlaceholder> {
class ImagePlaceholderState extends State<ImagePlaceholder> {
final controller = PopoverController();
late final editorState = context.read<EditorState>();
@@ -1,4 +1,7 @@
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/custom_image_block_component.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/image_placeholder.dart';
import 'package:appflowy_editor/appflowy_editor.dart' hide Log;
import 'package:flutter/material.dart';
final customImageMenuItem = SelectionMenuItem(
name: AppFlowyEditorLocalizations.current.image,
@@ -9,6 +12,54 @@ final customImageMenuItem = SelectionMenuItem(
),
keywords: ['image', 'picture', 'img', 'photo'],
handler: (editorState, menuService, context) async {
return await editorState.insertImageNode('');
// use the key to retrieve the state of the image block to show the popover automatically
final imagePlaceholderKey = GlobalKey<ImagePlaceholderState>();
await editorState.insertEmptyImageBlock(imagePlaceholderKey);
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
imagePlaceholderKey.currentState?.controller.show();
});
},
);
extension InsertImage on EditorState {
Future<void> insertEmptyImageBlock(GlobalKey key) async {
final selection = this.selection;
if (selection == null || !selection.isCollapsed) {
return;
}
final node = getNodeAtPath(selection.end.path);
if (node == null) {
return;
}
final emptyImage = imageNode(url: '')
..extraInfos = {
kImagePlaceholderKey: key,
};
final transaction = this.transaction;
// if the current node is empty paragraph, replace it with image node
if (node.type == ParagraphBlockKeys.type &&
(node.delta?.isEmpty ?? false)) {
transaction
..insertNode(
node.path,
emptyImage,
)
..deleteNode(node);
} else {
transaction.insertNode(
node.path.next,
emptyImage,
);
}
transaction.afterSelection = Selection.collapsed(
Position(
path: node.path.next,
offset: 0,
),
);
return apply(transaction);
}
}
@@ -3,14 +3,13 @@ import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
final ToolbarItem inlineMathEquationItem = ToolbarItem(
id: 'editor.inline_math_equation',
group: 2,
isActive: onlyShowInSingleSelectionAndTextType,
builder: (context, editorState, highlightColor) {
builder: (context, editorState, highlightColor, _) {
final selection = editorState.selection!;
final nodes = editorState.getNodesInSelection(selection);
final isHighlight = nodes.allSatisfyInSelection(selection, (delta) {
@@ -2,7 +2,7 @@ import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_block.dart';
import 'package:appflowy/plugins/document/presentation/more/cubit/document_appearance_cubit.dart';
import 'package:appflowy/user/application/reminder/reminder_bloc.dart';
import 'package:appflowy/workspace/application/appearance.dart';
import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
import 'package:appflowy/workspace/application/settings/date_time/date_format_ext.dart';
import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/date_picker_dialog.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
@@ -82,11 +82,13 @@ class MentionDateBlock extends StatelessWidget {
_updateBlock(parsedDate!.withoutTime, includeTime);
// We can remove time from the date/reminder
// block when toggled off.
if (!includeTime && isReminder) {
// block when toggled off.
if (isReminder) {
_updateScheduledAt(
reminderId: reminderId!,
selectedDay: parsedDate!.withoutTime,
selectedDay:
includeTime ? parsedDate! : parsedDate!.withoutTime,
includeTime: includeTime,
);
}
},
@@ -99,6 +101,7 @@ class MentionDateBlock extends StatelessWidget {
_updateScheduledAt(
reminderId: reminderId!,
selectedDay: selectedDay,
includeTime: includeTime,
);
}
},
@@ -171,10 +174,15 @@ class MentionDateBlock extends StatelessWidget {
void _updateScheduledAt({
required String reminderId,
required DateTime selectedDay,
bool? includeTime,
}) {
editorContext.read<ReminderBloc>().add(
ReminderEvent.update(
ReminderUpdate(id: reminderId, scheduledAt: selectedDay),
ReminderUpdate(
id: reminderId,
scheduledAt: selectedDay,
includeTime: includeTime,
),
),
);
}
@@ -14,7 +14,7 @@ final ToolbarItem smartEditItem = ToolbarItem(
id: 'appflowy.editor.smart_edit',
group: 0,
isActive: onlyShowInSingleSelectionAndTextType,
builder: (context, editorState, _) => SmartEditActionList(
builder: (context, editorState, _, __) => SmartEditActionList(
editorState: editorState,
),
);
@@ -55,9 +55,9 @@ class _SmartEditActionListState extends State<SmartEditActionList> {
actions: SmartEditAction.values
.map((action) => SmartEditActionWrapper(action))
.toList(),
onClosed: () => keepEditorFocusNotifier.value -= 1,
onClosed: () => keepEditorFocusNotifier.decrease(),
buildChild: (controller) {
keepEditorFocusNotifier.value += 1;
keepEditorFocusNotifier.increase();
return FlowyIconButton(
hoverColor: Colors.transparent,
tooltipText: isOpenAIEnabled
@@ -68,23 +68,16 @@ class OutlineBlockWidget extends BlockComponentStatefulWidget {
}
class _OutlineBlockWidgetState extends State<OutlineBlockWidget>
with BlockComponentConfigurable, BlockComponentTextDirectionMixin {
with
BlockComponentConfigurable,
BlockComponentTextDirectionMixin,
BlockComponentBackgroundColorMixin {
@override
BlockComponentConfiguration get configuration => widget.configuration;
@override
Node get node => widget.node;
// get the background color of the note block from the node's attributes
Color get backgroundColor {
final colorString =
node.attributes[OutlineBlockKeys.backgroundColor] as String?;
if (colorString == null) {
return Colors.transparent;
}
return colorString.tryToColor() ?? Colors.transparent;
}
@override
late EditorState editorState = context.read<EditorState>();
late Stream<(TransactionTime, Transaction)> stream =
@@ -0,0 +1,27 @@
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
class CalloutNodeParser extends NodeParser {
const CalloutNodeParser();
@override
String get id => CalloutBlockKeys.type;
@override
String transform(Node node, DocumentMarkdownEncoder? encoder) {
assert(node.children.isEmpty);
final icon = node.attributes[CalloutBlockKeys.icon];
final delta = node.delta ?? Delta()
..insert('');
final String markdown = DeltaMarkdownEncoder()
.convert(delta)
.split('\n')
.map((e) => '> $e')
.join('\n');
return '''
> $icon
$markdown
''';
}
}
@@ -1,13 +1,23 @@
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
class CodeBlockNodeParser extends NodeParser {
const CodeBlockNodeParser();
@override
String get id => 'code_block';
String get id => CodeBlockKeys.type;
@override
String transform(Node node) {
return '```\n${node.attributes['code_block']}\n```';
String transform(Node node, DocumentMarkdownEncoder? encoder) {
final delta = node.delta;
final language = node.attributes[CodeBlockKeys.language] ?? '';
if (delta == null) {
throw Exception('Delta is null');
}
final markdown = DeltaMarkdownEncoder().convert(delta);
final result = '```$language\n$markdown\n```';
final suffix = node.next == null ? '' : '\n';
return '$result$suffix';
}
}
@@ -0,0 +1,16 @@
import 'package:appflowy_editor/appflowy_editor.dart';
class CustomImageNodeParser extends NodeParser {
const CustomImageNodeParser();
@override
String get id => ImageBlockKeys.type;
@override
String transform(Node node, DocumentMarkdownEncoder? encoder) {
assert(node.children.isEmpty);
final url = node.attributes[ImageBlockKeys.url];
assert(url != null);
return '![]($url)\n';
}
}
@@ -4,10 +4,10 @@ class DividerNodeParser extends NodeParser {
const DividerNodeParser();
@override
String get id => 'divider';
String get id => DividerBlockKeys.type;
@override
String transform(Node node) {
String transform(Node node, DocumentMarkdownEncoder? encoder) {
return '---\n';
}
}
@@ -0,0 +1,6 @@
export 'callout_node_parser.dart';
export 'code_block_node_parser.dart';
export 'custom_image_node_parser.dart';
export 'divider_node_parser.dart';
export 'math_equation_node_parser.dart';
export 'toggle_list_node_parser.dart';
@@ -0,0 +1,5 @@
export 'callout_node_parser.dart';
export 'code_block_node_parser.dart';
export 'divider_node_parser.dart';
export 'math_equation_node_parser.dart';
export 'toggle_list_node_parser.dart';
@@ -8,7 +8,7 @@ class MathEquationNodeParser extends NodeParser {
String get id => MathEquationBlockKeys.type;
@override
String transform(Node node) {
String transform(Node node, DocumentMarkdownEncoder? encoder) {
return '\$\$${node.attributes[id]}\$\$';
}
}
@@ -0,0 +1,44 @@
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
enum ToggleListExportStyle {
github,
markdown,
}
class ToggleListNodeParser extends NodeParser {
const ToggleListNodeParser({
this.exportStyle = ToggleListExportStyle.markdown,
});
final ToggleListExportStyle exportStyle;
@override
String get id => ToggleListBlockKeys.type;
@override
String transform(Node node, DocumentMarkdownEncoder? encoder) {
final delta = node.delta ?? Delta()
..insert('');
String markdown = DeltaMarkdownEncoder().convert(delta);
final details = encoder?.convertNodes(
node.children,
withIndent: true,
);
switch (exportStyle) {
case ToggleListExportStyle.github:
return '''<details>
<summary>$markdown</summary>
$details
</details>
''';
case ToggleListExportStyle.markdown:
markdown = '- $markdown\n';
if (details != null && details.isNotEmpty) {
markdown += details;
}
return markdown;
}
}
}
@@ -28,6 +28,7 @@ export 'openai/widgets/auto_completion_node_widget.dart';
export 'openai/widgets/smart_edit_node_widget.dart';
export 'openai/widgets/smart_edit_toolbar_item.dart';
export 'outline/outline_block_component.dart';
export 'parsers/markdown_parsers.dart';
export 'table/table_menu.dart';
export 'table/table_option_action.dart';
export 'toggle/toggle_block_component.dart';
@@ -11,6 +11,8 @@ import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/style_widget/extension.dart';
import 'package:flutter/material.dart';
const tableCellDefaultColor = 'appflowy_table_cell_default_color';
enum TableOptionAction {
addAfter,
addBefore,
@@ -119,12 +121,14 @@ class TableColorOptionAction extends PopoverActionCell {
// reset to default background color
FlowyColorOption(
color: defaultColor,
name: LocaleKeys.document_plugins_optionAction_defaultColor.tr(),
i18n: LocaleKeys.document_plugins_optionAction_defaultColor.tr(),
id: tableCellDefaultColor,
),
...FlowyTint.values.map(
(e) => FlowyColorOption(
color: e.color(context),
name: e.tintName(AppFlowyEditorLocalizations.current),
i18n: e.tintName(AppFlowyEditorLocalizations.current),
id: e.id,
),
),
];
@@ -136,8 +140,9 @@ class TableColorOptionAction extends PopoverActionCell {
color: Theme.of(context).colorScheme.onBackground,
width: 1,
),
onTap: (color, index) async {
final backgroundColor = selectedColor != color ? color.toHex() : "";
onTap: (option, index) async {
final backgroundColor =
selectedColor != option.color ? option.id : '';
TableActions.setBgColor(
node,
position,
@@ -163,16 +163,19 @@ class _ToggleListBlockComponentWidgetState
textDirection: textDirection,
children: [
// the emoji picker button for the note
FlowyIconButton(
width: 24.0,
icon: Icon(
collapsed ? Icons.arrow_right : Icons.arrow_drop_down,
Container(
constraints: const BoxConstraints(minWidth: 26, minHeight: 22),
padding: const EdgeInsets.only(right: 4.0),
child: FlowyIconButton(
width: 18.0,
icon: Icon(
collapsed ? Icons.arrow_right : Icons.arrow_drop_down,
size: 18.0,
),
onPressed: onCollapsed,
),
onPressed: onCollapsed,
),
const SizedBox(
width: 4.0,
),
Flexible(
child: AppFlowyRichText(
key: forwardKey,
@@ -1,11 +1,8 @@
import 'package:appflowy/plugins/document/presentation/editor_plugins/inline_math_equation/inline_math_equation.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_block.dart';
import 'package:appflowy/plugins/document/presentation/more/cubit/document_appearance_cubit.dart';
import 'package:appflowy/plugins/inline_actions/inline_actions_menu.dart';
import 'package:appflowy/util/google_font_family_extension.dart';
import 'package:appflowy_editor/appflowy_editor.dart' hide Log;
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
@@ -63,12 +60,13 @@ class EditorStyleCustomizer {
color: theme.colorScheme.primary,
decoration: TextDecoration.underline,
),
code: GoogleFonts.robotoMono(
code: GoogleFonts.arefRuqaaInk(
textStyle: baseTextStyle(fontFamily).copyWith(
fontSize: fontSize,
fontWeight: FontWeight.normal,
fontStyle: FontStyle.italic,
color: Colors.red,
backgroundColor: theme.colorScheme.inverseSurface,
backgroundColor: theme.colorScheme.inverseSurface.withOpacity(0.8),
),
),
),
@@ -102,7 +100,7 @@ class EditorStyleCustomizer {
color: theme.colorScheme.primary,
decoration: TextDecoration.underline,
),
code: GoogleFonts.robotoMono(
code: GoogleFonts.arefRuqaaInk(
textStyle: baseTextStyle(fontFamily).copyWith(
fontSize: fontSize,
fontWeight: FontWeight.normal,
@@ -29,7 +29,7 @@ class DocumentMoreButton extends StatelessWidget {
),
];
},
child: FlowyTooltip.delayed(
child: FlowyTooltip(
message: LocaleKeys.moreAction_moreOptions.tr(),
child: FlowySvg(
FlowySvgs.details_s,
@@ -112,6 +112,7 @@ class ShareActionListState extends State<ShareActionList> {
return RoundedTextButton(
title: LocaleKeys.shareAction_buttonText.tr(),
onPressed: () => controller.show(),
textColor: Theme.of(context).colorScheme.onSurface,
);
},
onSelected: (action, controller) async {
@@ -4,6 +4,7 @@ import 'package:appflowy/plugins/document/application/doc_bloc.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/string_extension.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_block.dart';
import 'package:appflowy/plugins/inline_actions/inline_actions_result.dart';
import 'package:appflowy/user/application/reminder/reminder_extension.dart';
import 'package:appflowy/user/application/reminder/reminder_bloc.dart';
import 'package:appflowy_backend/protobuf/flowy-user/reminder.pb.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
@@ -209,7 +210,9 @@ class ReminderReferenceService {
objectId: viewId,
title: LocaleKeys.reminderNotification_title.tr(),
message: LocaleKeys.reminderNotification_message.tr(),
meta: {"document_id": viewId},
meta: {
ReminderMetaKeys.includeTime.name: false.toString(),
},
scheduledAt: Int64(date.millisecondsSinceEpoch ~/ 1000),
isAck: date.isBefore(DateTime.now()),
);
@@ -23,7 +23,8 @@ import 'package:appflowy/user/application/user_service.dart';
import 'package:appflowy/user/presentation/router.dart';
import 'package:appflowy/workspace/application/edit_panel/edit_panel_bloc.dart';
import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';
import 'package:appflowy/workspace/application/local_notifications/notification_action_bloc.dart';
import 'package:appflowy/workspace/application/notifications/notification_action_bloc.dart';
import 'package:appflowy/workspace/application/settings/notifications/notification_settings_cubit.dart';
import 'package:appflowy/workspace/application/settings/prelude.dart';
import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart';
import 'package:appflowy/workspace/application/user/prelude.dart';
@@ -168,14 +169,22 @@ void _resolveHomeDeps(GetIt getIt) {
getIt.registerLazySingleton<TabsBloc>(() => TabsBloc());
getIt.registerSingleton<ReminderBloc>(ReminderBloc());
getIt.registerSingleton<NotificationSettingsCubit>(
NotificationSettingsCubit(),
);
getIt.registerSingleton<ReminderBloc>(
ReminderBloc(notificationSettings: getIt<NotificationSettingsCubit>()),
);
}
void _resolveFolderDeps(GetIt getIt) {
//workspace
getIt.registerFactoryParam<WorkspaceListener, UserProfilePB, String>(
(user, workspaceId) =>
WorkspaceListener(user: user, workspaceId: workspaceId),
(user, workspaceId) => WorkspaceListener(
user: user,
workspaceId: workspaceId,
),
);
getIt.registerFactoryParam<ViewBloc, ViewPB, void>(
@@ -10,6 +10,7 @@ import 'deps_resolver.dart';
import 'entry_point.dart';
import 'launch_configuration.dart';
import 'plugin/plugin.dart';
import 'tasks/appflowy_cloud_task.dart';
import 'tasks/prelude.dart';
final getIt = GetIt.instance;
@@ -71,6 +72,7 @@ class FlowyRunner {
if (!mode.isUnitTest) ...[
const HotKeyTask(),
InitSupabaseTask(),
InitAppFlowyCloudTask(),
const InitAppWidgetTask(),
const InitPlatformServiceTask()
],
@@ -1,19 +1,23 @@
import 'package:appflowy/plugins/document/presentation/more/cubit/document_appearance_cubit.dart';
import 'package:appflowy/workspace/application/local_notifications/notification_service.dart';
import 'package:appflowy/startup/tasks/prelude.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:appflowy_editor/appflowy_editor.dart' hide Log;
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/theme.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:appflowy/workspace/application/settings/notifications/notification_settings_cubit.dart';
import 'prelude.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
import '../../user/application/user_settings_service.dart';
import '../../workspace/application/appearance.dart';
import '../startup.dart';
import 'package:go_router/go_router.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:appflowy_editor/appflowy_editor.dart' hide Log;
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:flowy_infra/theme.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:appflowy/plugins/document/presentation/more/cubit/document_appearance_cubit.dart';
import 'package:appflowy/workspace/application/notifications/notification_service.dart';
import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
import 'package:appflowy/user/application/user_settings_service.dart';
import 'package:appflowy/startup/startup.dart';
class InitAppWidgetTask extends LaunchTask {
const InitAppWidgetTask();
@@ -68,9 +72,11 @@ class InitAppWidgetTask extends LaunchTask {
Locale('tr', 'TR'),
Locale('uk', 'UA'),
Locale('ur'),
Locale('vi', 'VN'),
Locale('zh', 'CN'),
Locale('zh', 'TW'),
Locale('fa'),
Locale('hin'),
],
path: 'assets/translations',
fallbackLocale: const Locale('en'),
@@ -94,8 +100,8 @@ class ApplicationWidget extends StatefulWidget {
});
final Widget child;
final AppearanceSettingsPB appearanceSetting;
final AppTheme appTheme;
final AppearanceSettingsPB appearanceSetting;
final DateTimeSettingsPB dateTimeSettings;
@override
@@ -124,6 +130,9 @@ class _ApplicationWidgetState extends State<ApplicationWidget> {
widget.appTheme,
)..readLocaleWhenAppLaunch(context),
),
BlocProvider<NotificationSettingsCubit>(
create: (_) => getIt<NotificationSettingsCubit>(),
),
BlocProvider<DocumentAppearanceCubit>(
create: (_) => DocumentAppearanceCubit()..fetch(),
),
@@ -0,0 +1,29 @@
import 'package:appflowy/env/env.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/application/auth/auth_service.dart';
import 'package:appflowy/user/application/user_auth_listener.dart';
class InitAppFlowyCloudTask extends LaunchTask {
final _authStateListener = UserAuthStateListener();
bool isLoggingOut = false;
@override
Future<void> initialize(LaunchContext context) async {
if (!isAppFlowyCloudEnabled) {
return;
}
_authStateListener.start(
didSignIn: () {
isLoggingOut = false;
},
onInvalidAuth: (message) async {
await getIt<AuthService>().signOut();
// TODO(nathan): Show a dialog to notify the user that the session is expired.
if (!isLoggingOut) {
await runAppFlowy();
}
},
);
}
}
@@ -0,0 +1,38 @@
import 'package:bloc/bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'notification_filter_bloc.freezed.dart';
class NotificationFilterBloc
extends Bloc<NotificationFilterEvent, NotificationFilterState> {
NotificationFilterBloc() : super(const NotificationFilterState()) {
on<NotificationFilterEvent>((event, emit) async {
event.when(
reset: () => emit(const NotificationFilterState()),
toggleShowUnreadsOnly: () => emit(
state.copyWith(showUnreadsOnly: !state.showUnreadsOnly),
),
);
});
}
}
@freezed
class NotificationFilterEvent with _$NotificationFilterEvent {
const factory NotificationFilterEvent.toggleShowUnreadsOnly() =
_ToggleShowUnreadsOnly;
const factory NotificationFilterEvent.reset() = _Reset;
}
@freezed
class NotificationFilterState with _$NotificationFilterState {
const NotificationFilterState._();
const factory NotificationFilterState({
@Default(false) bool showUnreadsOnly,
}) = _NotificationFilterState;
// If state is not default values, then there are custom changes
bool get hasFilters => showUnreadsOnly != false;
}
@@ -2,10 +2,12 @@ import 'dart:async';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/application/reminder/reminder_extension.dart';
import 'package:appflowy/user/application/reminder/reminder_service.dart';
import 'package:appflowy/workspace/application/local_notifications/notification_action.dart';
import 'package:appflowy/workspace/application/local_notifications/notification_action_bloc.dart';
import 'package:appflowy/workspace/application/local_notifications/notification_service.dart';
import 'package:appflowy/workspace/application/notifications/notification_action.dart';
import 'package:appflowy/workspace/application/notifications/notification_action_bloc.dart';
import 'package:appflowy/workspace/application/notifications/notification_service.dart';
import 'package:appflowy/workspace/application/settings/notifications/notification_settings_cubit.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:bloc/bloc.dart';
@@ -18,35 +20,58 @@ import 'package:freezed_annotation/freezed_annotation.dart';
part 'reminder_bloc.freezed.dart';
class ReminderBloc extends Bloc<ReminderEvent, ReminderState> {
final NotificationSettingsCubit _notificationSettings;
late final NotificationActionBloc actionBloc;
late final ReminderService reminderService;
late final Timer timer;
ReminderBloc() : super(ReminderState()) {
ReminderBloc({
required NotificationSettingsCubit notificationSettings,
}) : _notificationSettings = notificationSettings,
super(ReminderState()) {
actionBloc = getIt<NotificationActionBloc>();
reminderService = const ReminderService();
timer = _periodicCheck();
on<ReminderEvent>((event, emit) async {
await event.when(
markAllRead: () async {
final unreadReminders =
state.pastReminders.where((reminder) => !reminder.isRead);
final reminders = [...state.reminders];
final updatedReminders = <ReminderPB>[];
for (final reminder in unreadReminders) {
reminders.remove(reminder);
reminder.isRead = true;
await reminderService.updateReminder(reminder: reminder);
updatedReminders.add(reminder);
}
reminders.addAll(updatedReminders);
emit(state.copyWith(reminders: reminders));
},
started: () async {
final remindersOrFailure = await reminderService.fetchReminders();
remindersOrFailure.fold(
(error) => Log.error(error),
(reminders) => _updateState(emit, reminders),
(reminders) => emit(state.copyWith(reminders: reminders)),
);
},
remove: (reminderId) async {
remove: (reminder) async {
final unitOrFailure =
await reminderService.removeReminder(reminderId: reminderId);
await reminderService.removeReminder(reminderId: reminder.id);
unitOrFailure.fold(
(error) => Log.error(error),
(_) {
final reminders = [...state.reminders];
reminders.removeWhere((e) => e.id == reminderId);
_updateState(emit, reminders);
reminders.removeWhere((e) => e.id == reminder.id);
emit(state.copyWith(reminders: reminders));
},
);
},
@@ -57,8 +82,8 @@ class ReminderBloc extends Bloc<ReminderEvent, ReminderState> {
return unitOrFailure.fold(
(error) => Log.error(error),
(_) {
state.reminders.add(reminder);
_updateState(emit, state.reminders);
final reminders = [...state.reminders, reminder];
emit(state.copyWith(reminders: reminders));
},
);
},
@@ -82,7 +107,7 @@ class ReminderBloc extends Bloc<ReminderEvent, ReminderState> {
state.reminders.indexWhere((r) => r.id == reminder.id);
final reminders = [...state.reminders];
reminders.replaceRange(index, index + 1, [newReminder]);
_updateState(emit, reminders);
emit(state.copyWith(reminders: reminders));
},
);
},
@@ -108,23 +133,13 @@ class ReminderBloc extends Bloc<ReminderEvent, ReminderState> {
});
}
void _updateState(Emitter emit, List<ReminderPB> reminders) {
final now = DateTime.now();
final hasUnreads = reminders.any(
(r) =>
DateTime.fromMillisecondsSinceEpoch(r.scheduledAt.toInt() * 1000)
.isBefore(now) &&
!r.isRead,
);
emit(state.copyWith(reminders: reminders, hasUnreads: hasUnreads));
}
Timer _periodicCheck() {
return Timer.periodic(
const Duration(minutes: 1),
(_) {
final now = DateTime.now();
for (final reminder in state.reminders) {
for (final reminder in state.upcomingReminders) {
if (reminder.isAck) {
continue;
}
@@ -134,16 +149,18 @@ class ReminderBloc extends Bloc<ReminderEvent, ReminderState> {
);
if (scheduledAt.isBefore(now)) {
NotificationMessage(
identifier: reminder.id,
title: LocaleKeys.reminderNotification_title.tr(),
body: LocaleKeys.reminderNotification_message.tr(),
onClick: () => actionBloc.add(
NotificationActionEvent.performAction(
action: NotificationAction(objectId: reminder.objectId),
if (_notificationSettings.state.isNotificationsEnabled) {
NotificationMessage(
identifier: reminder.id,
title: LocaleKeys.reminderNotification_title.tr(),
body: LocaleKeys.reminderNotification_message.tr(),
onClick: () => actionBloc.add(
NotificationActionEvent.performAction(
action: NotificationAction(objectId: reminder.objectId),
),
),
),
);
);
}
add(
ReminderEvent.update(
@@ -163,7 +180,7 @@ class ReminderEvent with _$ReminderEvent {
const factory ReminderEvent.started() = _Started;
// Remove a reminder
const factory ReminderEvent.remove({required String reminderId}) = _Remove;
const factory ReminderEvent.remove({required ReminderPB reminder}) = _Remove;
// Add a reminder
const factory ReminderEvent.add({required ReminderPB reminder}) = _Add;
@@ -171,6 +188,9 @@ class ReminderEvent with _$ReminderEvent {
// Update a reminder (eg. isAck, isRead, etc.)
const factory ReminderEvent.update(ReminderUpdate update) = _Update;
// Mark all unread reminders as read
const factory ReminderEvent.markAllRead() = _MarkAllRead;
const factory ReminderEvent.pressReminder({required String reminderId}) =
_PressReminder;
}
@@ -183,12 +203,14 @@ class ReminderUpdate {
final bool? isAck;
final bool? isRead;
final DateTime? scheduledAt;
final bool? includeTime;
ReminderUpdate({
required this.id,
this.isAck,
this.isRead,
this.scheduledAt,
this.includeTime,
});
ReminderPB merge({required ReminderPB a}) {
@@ -196,6 +218,11 @@ class ReminderUpdate {
? scheduledAt!.isBefore(DateTime.now())
: a.isAck;
final meta = a.meta;
if (includeTime != a.includeTime) {
meta[ReminderMetaKeys.includeTime.name] = includeTime.toString();
}
return ReminderPB(
id: a.id,
objectId: a.objectId,
@@ -206,27 +233,52 @@ class ReminderUpdate {
isRead: isRead ?? a.isRead,
title: a.title,
message: a.message,
meta: a.meta,
meta: meta,
);
}
}
class ReminderState {
ReminderState({
List<ReminderPB>? reminders,
bool? hasUnreads,
}) : reminders = reminders ?? [],
hasUnreads = hasUnreads ?? false;
ReminderState({List<ReminderPB>? reminders}) {
_reminders = reminders ?? [];
final List<ReminderPB> reminders;
final bool hasUnreads;
pastReminders = [];
upcomingReminders = [];
ReminderState copyWith({
List<ReminderPB>? reminders,
bool? hasUnreads,
}) =>
ReminderState(
reminders: reminders ?? this.reminders,
hasUnreads: hasUnreads ?? this.hasUnreads,
if (_reminders.isEmpty) {
hasUnreads = false;
return;
}
final now = DateTime.now();
bool hasUnreadReminders = false;
for (final reminder in _reminders) {
final scheduledDate = DateTime.fromMillisecondsSinceEpoch(
reminder.scheduledAt.toInt() * 1000,
);
if (scheduledDate.isBefore(now)) {
pastReminders.add(reminder);
if (!hasUnreadReminders && !reminder.isRead) {
hasUnreadReminders = true;
}
} else {
upcomingReminders.add(reminder);
}
}
hasUnreads = hasUnreadReminders;
}
late final List<ReminderPB> _reminders;
List<ReminderPB> get reminders => _reminders;
late final List<ReminderPB> pastReminders;
late final List<ReminderPB> upcomingReminders;
late final bool hasUnreads;
ReminderState copyWith({List<ReminderPB>? reminders}) =>
ReminderState(reminders: reminders ?? _reminders);
}
@@ -0,0 +1,17 @@
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
enum ReminderMetaKeys {
includeTime("include_time");
const ReminderMetaKeys(this.name);
final String name;
}
extension ReminderExtension on ReminderPB {
bool? get includeTime {
final String? includeTimeStr = meta[ReminderMetaKeys.includeTime.name];
return includeTimeStr != null ? includeTimeStr == true.toString() : null;
}
}
@@ -35,7 +35,7 @@ class SupbaseRealtimeService {
_subscribeTablesChanges();
isLoggingOut = false;
},
onForceLogout: (message) async {
onInvalidAuth: (message) async {
await getIt<AuthService>().signOut();
channel?.unsubscribe();
channel = null;
@@ -1,26 +1,27 @@
import 'dart:async';
import 'dart:typed_data';
import 'package:appflowy/core/notification/user_notification.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-user/auth.pb.dart';
import 'package:dartz/dartz.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'dart:typed_data';
import 'package:appflowy_backend/protobuf/flowy-notification/protobuf.dart';
import 'package:appflowy_backend/protobuf/flowy-user/auth.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-user/notification.pb.dart'
as user;
import 'package:appflowy_backend/rust_stream.dart';
import 'package:dartz/dartz.dart';
class UserAuthStateListener {
void Function(String)? _onForceLogout;
void Function(String)? _onInvalidAuth;
void Function()? _didSignIn;
StreamSubscription<SubscribeObject>? _subscription;
UserNotificationParser? _userParser;
void start({
void Function(String)? onForceLogout,
void Function(String)? onInvalidAuth,
void Function()? didSignIn,
}) {
_onForceLogout = onForceLogout;
_onInvalidAuth = onInvalidAuth;
_didSignIn = didSignIn;
_userParser = UserNotificationParser(
@@ -35,7 +36,7 @@ class UserAuthStateListener {
Future<void> stop() async {
_userParser = null;
await _subscription?.cancel();
_onForceLogout = null;
_onInvalidAuth = null;
}
void _userNotificationCallback(
@@ -51,8 +52,8 @@ class UserAuthStateListener {
case AuthStatePB.AuthStateSignIn:
_didSignIn?.call();
break;
case AuthStatePB.AuthStateForceSignOut:
_onForceLogout?.call("");
case AuthStatePB.InvalidAuth:
_onInvalidAuth?.call("");
break;
default:
break;
@@ -41,4 +41,20 @@ class UserSettingsBackendService {
) async {
return (await UserEventSetDateTimeSettings(settings).send()).swap();
}
Future<Either<FlowyError, Unit>> setNotificationSettings(
NotificationSettingsPB settings,
) async {
return (await UserEventSetNotificationSettings(settings).send()).swap();
}
Future<NotificationSettingsPB> getNotificationSettings() async {
final result = await UserEventGetNotificationSettings().send();
return result.fold(
(NotificationSettingsPB setting) => setting,
(error) =>
throw FlowySDKException(ExceptionType.AppearanceSettingsIsEmpty),
);
}
}
@@ -6,7 +6,7 @@ import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/application/sign_in_bloc.dart';
import 'package:appflowy/user/presentation/presentation.dart';
import 'package:appflowy/util/platform_extension.dart';
import 'package:appflowy/workspace/application/appearance.dart';
import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/size.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
@@ -8,7 +8,7 @@ import 'package:appflowy/user/application/auth/auth_service.dart';
import 'package:appflowy/user/application/historical_user_bloc.dart';
import 'package:appflowy/user/presentation/router.dart';
import 'package:appflowy/user/presentation/widgets/widgets.dart';
import 'package:appflowy/workspace/application/appearance.dart';
import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/settings_language_view.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart';
@@ -3,15 +3,22 @@ import 'dart:convert';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/application/document_data_pb_extension.dart';
import 'package:appflowy/plugins/document/application/prelude.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/parsers/code_block_node_parser.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/parsers/divider_node_parser.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/parsers/math_equation_node_parser.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/parsers/document_markdown_parsers.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:dartz/dartz.dart';
import 'package:easy_localization/easy_localization.dart';
const List<NodeParser> _customParsers = [
DividerNodeParser(),
MathEquationNodeParser(),
CodeBlockNodeParser(),
CalloutNodeParser(),
ToggleListNodeParser(),
CustomImageNodeParser(),
];
enum DocumentExportType {
json,
markdown,
@@ -43,11 +50,7 @@ class DocumentExporter {
case DocumentExportType.markdown:
final markdown = documentToMarkdown(
document,
customParsers: [
const DividerNodeParser(),
const MathEquationNodeParser(),
const CodeBlockNodeParser(),
],
customParsers: _customParsers,
);
return right(markdown);
case DocumentExportType.text:
@@ -1,10 +1,11 @@
import 'package:appflowy/user/application/user_listener.dart';
import 'package:appflowy/workspace/application/appearance.dart';
import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
import 'package:appflowy/workspace/application/edit_panel/edit_context.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/workspace.pb.dart'
show WorkspaceSettingPB;
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
import 'package:dartz/dartz.dart';
import 'package:flowy_infra/size.dart';
import 'package:flowy_infra/time/duration.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
@@ -19,12 +20,14 @@ class HomeSettingBloc extends Bloc<HomeSettingEvent, HomeSettingState> {
UserProfilePB user,
WorkspaceSettingPB workspaceSetting,
AppearanceSettingsCubit appearanceSettingsCubit,
double screenWidthPx,
) : _listener = UserWorkspaceListener(userProfile: user),
_appearanceSettingsCubit = appearanceSettingsCubit,
super(
HomeSettingState.initial(
workspaceSetting,
appearanceSettingsCubit.state,
screenWidthPx,
),
) {
on<HomeSettingEvent>(
@@ -43,7 +46,28 @@ class HomeSettingBloc extends Bloc<HomeSettingEvent, HomeSettingState> {
collapseMenu: (_CollapseMenu e) {
final isMenuCollapsed = !state.isMenuCollapsed;
_appearanceSettingsCubit.saveIsMenuCollapsed(isMenuCollapsed);
emit(state.copyWith(isMenuCollapsed: isMenuCollapsed));
emit(
state.copyWith(
isMenuCollapsed: isMenuCollapsed,
keepMenuCollapsed: isMenuCollapsed,
),
);
},
checkScreenSize: (_CheckScreenSize e) {
final bool isScreenSmall =
e.screenWidthPx < PageBreaks.tabletLandscape;
if (state.isScreenSmall != isScreenSmall) {
final isMenuCollapsed = isScreenSmall || state.keepMenuCollapsed;
emit(
state.copyWith(
isMenuCollapsed: isMenuCollapsed,
isScreenSmall: isScreenSmall,
),
);
} else {
emit(state.copyWith(isScreenSmall: isScreenSmall));
}
},
editPanelResizeStart: (_EditPanelResizeStart e) {
emit(
@@ -102,6 +126,8 @@ class HomeSettingEvent with _$HomeSettingEvent {
WorkspaceSettingPB setting,
) = _DidReceiveWorkspaceSetting;
const factory HomeSettingEvent.collapseMenu() = _CollapseMenu;
const factory HomeSettingEvent.checkScreenSize(double screenWidthPx) =
_CheckScreenSize;
const factory HomeSettingEvent.editPanelResized(double offset) =
_EditPanelResized;
const factory HomeSettingEvent.editPanelResizeStart() = _EditPanelResizeStart;
@@ -115,6 +141,8 @@ class HomeSettingState with _$HomeSettingState {
required WorkspaceSettingPB workspaceSetting,
required bool unauthorized,
required bool isMenuCollapsed,
required bool keepMenuCollapsed,
required bool isScreenSmall,
required double resizeOffset,
required double resizeStart,
required MenuResizeType resizeType,
@@ -123,14 +151,18 @@ class HomeSettingState with _$HomeSettingState {
factory HomeSettingState.initial(
WorkspaceSettingPB workspaceSetting,
AppearanceSettingsState appearanceSettingsState,
) =>
HomeSettingState(
panelContext: none(),
workspaceSetting: workspaceSetting,
unauthorized: false,
isMenuCollapsed: appearanceSettingsState.isMenuCollapsed,
resizeOffset: appearanceSettingsState.menuOffset,
resizeStart: 0,
resizeType: MenuResizeType.slide,
);
double screenWidthPx,
) {
return HomeSettingState(
panelContext: none(),
workspaceSetting: workspaceSetting,
unauthorized: false,
isMenuCollapsed: appearanceSettingsState.isMenuCollapsed,
isScreenSmall: screenWidthPx < PageBreaks.tabletLandscape,
keepMenuCollapsed: false,
resizeOffset: appearanceSettingsState.menuOffset,
resizeStart: 0,
resizeType: MenuResizeType.slide,
);
}
}
@@ -1,4 +1,4 @@
import 'package:appflowy/workspace/application/local_notifications/notification_action.dart';
import 'package:appflowy/workspace/application/notifications/notification_action.dart';
import 'package:bloc/bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
@@ -16,33 +16,40 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:google_fonts/google_fonts.dart';
part 'appearance.freezed.dart';
part 'appearance_cubit.freezed.dart';
const _white = Color(0xFFFFFFFF);
/// [AppearanceSettingsCubit] is used to modify the appearance of AppFlowy.
/// It includes the [AppTheme], [ThemeMode], [TextStyles] and [Locale].
/// It includes:
/// - [AppTheme]
/// - [ThemeMode]
/// - [TextStyle]'s
/// - [Locale]
/// - [UserDateFormatPB]
/// - [UserTimeFormatPB]
///
class AppearanceSettingsCubit extends Cubit<AppearanceSettingsState> {
final AppearanceSettingsPB _setting;
final AppearanceSettingsPB _appearanceSettings;
final DateTimeSettingsPB _dateTimeSettings;
AppearanceSettingsCubit(
AppearanceSettingsPB setting,
AppearanceSettingsPB appearanceSettings,
DateTimeSettingsPB dateTimeSettings,
AppTheme appTheme,
) : _setting = setting,
) : _appearanceSettings = appearanceSettings,
_dateTimeSettings = dateTimeSettings,
super(
AppearanceSettingsState.initial(
appTheme,
setting.themeMode,
setting.font,
setting.monospaceFont,
setting.layoutDirection,
setting.textDirection,
setting.locale,
setting.isMenuCollapsed,
setting.menuOffset,
appearanceSettings.themeMode,
appearanceSettings.font,
appearanceSettings.monospaceFont,
appearanceSettings.layoutDirection,
appearanceSettings.textDirection,
appearanceSettings.locale,
appearanceSettings.isMenuCollapsed,
appearanceSettings.menuOffset,
dateTimeSettings.dateFormat,
dateTimeSettings.timeFormat,
dateTimeSettings.timezoneId,
@@ -52,7 +59,7 @@ class AppearanceSettingsCubit extends Cubit<AppearanceSettingsState> {
/// Update selected theme in the user's settings and emit an updated state
/// with the AppTheme named [themeName].
Future<void> setTheme(String themeName) async {
_setting.theme = themeName;
_appearanceSettings.theme = themeName;
_saveAppearanceSettings();
emit(state.copyWith(appTheme: await AppTheme.fromName(themeName)));
}
@@ -63,7 +70,7 @@ class AppearanceSettingsCubit extends Cubit<AppearanceSettingsState> {
/// Update the theme mode in the user's settings and emit an updated state.
void setThemeMode(ThemeMode themeMode) {
_setting.themeMode = _themeModeToPB(themeMode);
_appearanceSettings.themeMode = _themeModeToPB(themeMode);
_saveAppearanceSettings();
emit(state.copyWith(themeMode: themeMode));
}
@@ -81,13 +88,13 @@ class AppearanceSettingsCubit extends Cubit<AppearanceSettingsState> {
}
void setLayoutDirection(LayoutDirection layoutDirection) {
_setting.layoutDirection = layoutDirection.toLayoutDirectionPB();
_appearanceSettings.layoutDirection = layoutDirection.toLayoutDirectionPB();
_saveAppearanceSettings();
emit(state.copyWith(layoutDirection: layoutDirection));
}
void setTextDirection(AppFlowyTextDirection? textDirection) {
_setting.textDirection =
_appearanceSettings.textDirection =
textDirection?.toTextDirectionPB() ?? TextDirectionPB.FALLBACK;
_saveAppearanceSettings();
emit(state.copyWith(textDirection: textDirection));
@@ -96,7 +103,7 @@ class AppearanceSettingsCubit extends Cubit<AppearanceSettingsState> {
/// Update selected font in the user's settings and emit an updated state
/// with the font name.
void setFontFamily(String fontFamilyName) {
_setting.font = fontFamilyName;
_appearanceSettings.font = fontFamilyName;
_saveAppearanceSettings();
emit(state.copyWith(font: fontFamilyName));
}
@@ -118,8 +125,8 @@ class AppearanceSettingsCubit extends Cubit<AppearanceSettingsState> {
});
if (state.locale != newLocale) {
_setting.locale.languageCode = newLocale.languageCode;
_setting.locale.countryCode = newLocale.countryCode ?? "";
_appearanceSettings.locale.languageCode = newLocale.languageCode;
_appearanceSettings.locale.countryCode = newLocale.countryCode ?? "";
_saveAppearanceSettings();
emit(state.copyWith(locale: newLocale));
}
@@ -127,13 +134,13 @@ class AppearanceSettingsCubit extends Cubit<AppearanceSettingsState> {
// Saves the menus current visibility
void saveIsMenuCollapsed(bool collapsed) {
_setting.isMenuCollapsed = collapsed;
_appearanceSettings.isMenuCollapsed = collapsed;
_saveAppearanceSettings();
}
// Saves the current resize offset of the menu
void saveMenuOffset(double offset) {
_setting.menuOffset = offset;
_appearanceSettings.menuOffset = offset;
_saveAppearanceSettings();
}
@@ -146,14 +153,14 @@ class AppearanceSettingsCubit extends Cubit<AppearanceSettingsState> {
}
if (value == null) {
_setting.settingKeyValue.remove(key);
_appearanceSettings.settingKeyValue.remove(key);
}
if (_setting.settingKeyValue[key] != value) {
if (_appearanceSettings.settingKeyValue[key] != value) {
if (value == null) {
_setting.settingKeyValue.remove(key);
_appearanceSettings.settingKeyValue.remove(key);
} else {
_setting.settingKeyValue[key] = value;
_appearanceSettings.settingKeyValue[key] = value;
}
}
_saveAppearanceSettings();
@@ -164,14 +171,14 @@ class AppearanceSettingsCubit extends Cubit<AppearanceSettingsState> {
Log.warn("The key should not be empty");
return null;
}
return _setting.settingKeyValue[key];
return _appearanceSettings.settingKeyValue[key];
}
/// Called when the application launches.
/// Uses the device locale when the application is opened for the first time.
void readLocaleWhenAppLaunch(BuildContext context) {
if (_setting.resetToDefault) {
_setting.resetToDefault = false;
if (_appearanceSettings.resetToDefault) {
_appearanceSettings.resetToDefault = false;
_saveAppearanceSettings();
setLocale(context, context.deviceLocale);
return;
@@ -204,7 +211,9 @@ class AppearanceSettingsCubit extends Cubit<AppearanceSettingsState> {
}
Future<void> _saveAppearanceSettings() async {
UserSettingsBackendService().setAppearanceSetting(_setting).then((result) {
UserSettingsBackendService()
.setAppearanceSetting(_appearanceSettings)
.then((result) {
result.fold(
(l) => null,
(error) => Log.error(error),
@@ -0,0 +1,62 @@
import 'dart:async';
import 'package:appflowy/user/application/user_settings_service.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-user/user_setting.pb.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'notification_settings_cubit.freezed.dart';
class NotificationSettingsCubit extends Cubit<NotificationSettingsState> {
final Completer<void> _initCompleter = Completer();
late final NotificationSettingsPB _notificationSettings;
NotificationSettingsCubit() : super(NotificationSettingsState.initial()) {
UserSettingsBackendService()
.getNotificationSettings()
.then((notificationSettings) {
_notificationSettings = notificationSettings;
_initCompleter.complete();
});
}
Future<void> toggleNotificationsEnabled() async {
await _initCompleter.future;
_notificationSettings.notificationsEnabled = !state.isNotificationsEnabled;
_saveNotificationSettings();
emit(
state.copyWith(
isNotificationsEnabled: _notificationSettings.notificationsEnabled,
),
);
}
Future<void> _saveNotificationSettings() async {
await _initCompleter.future;
UserSettingsBackendService()
.setNotificationSettings(_notificationSettings)
.then((result) {
result.fold(
(error) => Log.error(error),
(r) => null,
);
});
}
}
@freezed
class NotificationSettingsState with _$NotificationSettingsState {
const NotificationSettingsState._();
const factory NotificationSettingsState({
required bool isNotificationsEnabled,
}) = _NotificationSettingsState;
factory NotificationSettingsState.initial() =>
const NotificationSettingsState(isNotificationsEnabled: true);
}
@@ -13,6 +13,7 @@ enum SettingsPage {
language,
files,
user,
notifications,
syncSetting,
shortcuts,
}
@@ -3,7 +3,7 @@ import 'package:appflowy/startup/plugin/plugin.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/application/auth/auth_service.dart';
import 'package:appflowy/user/application/reminder/reminder_bloc.dart';
import 'package:appflowy/workspace/application/appearance.dart';
import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
import 'package:appflowy/workspace/application/home/home_bloc.dart';
import 'package:appflowy/workspace/application/home/home_service.dart';
import 'package:appflowy/workspace/application/home/home_setting_bloc.dart';
@@ -23,6 +23,7 @@ import 'package:flowy_infra_ui/style_widget/container.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:sized_context/sized_context.dart';
import 'package:styled_widget/styled_widget.dart';
import '../widgets/edit_panel/edit_panel.dart';
@@ -75,11 +76,12 @@ class DesktopHomeScreen extends StatelessWidget {
},
),
BlocProvider<HomeSettingBloc>(
create: (context) {
create: (_) {
return HomeSettingBloc(
userProfile,
workspaceSetting,
context.read<AppearanceSettingsCubit>(),
context.widthPx,
)..add(const HomeSettingEvent.initial());
},
),
@@ -29,6 +29,11 @@ class HomeLayout {
menuWidth += homeSetting.resizeOffset;
final screenWidthPx = context.widthPx;
context
.read<HomeSettingBloc>()
.add(HomeSettingEvent.checkScreenSize(screenWidthPx));
if (homeSetting.isMenuCollapsed) {
showMenu = false;
} else {
@@ -1,5 +1,5 @@
import 'dart:io';
import 'package:appflowy/workspace/application/appearance.dart';
import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
import 'package:appflowy/workspace/application/home/home_setting_bloc.dart';
import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart';
import 'package:flutter/material.dart';
@@ -1,7 +1,7 @@
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';
import 'package:appflowy/workspace/application/local_notifications/notification_action.dart';
import 'package:appflowy/workspace/application/local_notifications/notification_action_bloc.dart';
import 'package:appflowy/workspace/application/notifications/notification_action.dart';
import 'package:appflowy/workspace/application/notifications/notification_action_bloc.dart';
import 'package:appflowy/workspace/application/menu/menu_bloc.dart';
import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_folder.dart';
@@ -70,7 +70,7 @@ class SidebarTopMenu extends StatelessWidget {
),
],
);
return FlowyTooltip.delayed(
return FlowyTooltip(
richMessage: textSpan,
child: FlowyIconButton(
width: 28,
@@ -2,8 +2,9 @@ import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/plugins/document/presentation/more/cubit/document_appearance_cubit.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/workspace/application/menu/menu_user_bloc.dart';
import 'package:appflowy/workspace/presentation/notifications/notification_button.dart';
import 'package:appflowy/workspace/presentation/notifications/widgets/notification_button.dart';
import 'package:appflowy/workspace/presentation/settings/settings_dialog.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
import 'package:appflowy/workspace/presentation/widgets/user_avatar.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
@@ -65,7 +66,7 @@ class SidebarUser extends StatelessWidget {
Widget _buildSettingsButton(BuildContext context, MenuUserState state) {
final userProfile = state.userProfile;
return FlowyTooltip.delayed(
return FlowyTooltip(
message: LocaleKeys.settings_menu_open.tr(),
child: IconButton(
onPressed: () {
@@ -81,7 +82,13 @@ class SidebarUser extends StatelessWidget {
Navigator.of(dialogContext).pop();
await runAppFlowy();
},
dismissDialog: () => Navigator.of(context).pop(),
dismissDialog: () {
if (Navigator.of(dialogContext).canPop()) {
Navigator.of(dialogContext).pop();
} else {
Log.warn("Can't pop dialog context");
}
},
didOpenUser: () async {
// Pop the dialog using the dialog context
Navigator.of(dialogContext).pop();
@@ -339,7 +339,7 @@ class _SingleInnerViewItemState extends State<SingleInnerViewItem> {
// + button
Widget _buildViewAddButton(BuildContext context) {
return FlowyTooltip.delayed(
return FlowyTooltip(
message: LocaleKeys.menuAppHeader_addPageTooltip.tr(),
child: ViewAddButton(
parentViewId: widget.view.id,
@@ -379,7 +379,7 @@ class _SingleInnerViewItemState extends State<SingleInnerViewItem> {
// ··· more action button
Widget _buildViewMoreActionButton(BuildContext context) {
return FlowyTooltip.delayed(
return FlowyTooltip(
message: LocaleKeys.menuAppHeader_moreButtonToolTip.tr(),
child: ViewMoreActionButton(
view: widget.view,
@@ -66,7 +66,7 @@ class FlowyNavigation extends StatelessWidget {
if (state.isMenuCollapsed) {
return RotationTransition(
turns: const AlwaysStoppedAnimation(180 / 360),
child: FlowyTooltip.delayed(
child: FlowyTooltip(
richMessage: sidebarTooltipTextSpan(
context,
LocaleKeys.sideBar_openSidebar.tr(),
@@ -1,25 +1,29 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/application/notification_filter/notification_filter_bloc.dart';
import 'package:appflowy/user/application/reminder/reminder_bloc.dart';
import 'package:appflowy/workspace/presentation/notifications/notification_item.dart';
import 'package:appflowy/workspace/presentation/notifications/widgets/notification_hub_title.dart';
import 'package:appflowy/workspace/presentation/notifications/widgets/notification_tab_bar.dart';
import 'package:appflowy/workspace/presentation/notifications/widgets/notification_view.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-user/reminder.pb.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:collection/collection.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:flowy_infra/size.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/style_widget/hover.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
extension _ReminderReady on ReminderPB {
DateTime get scheduledDate =>
DateTime.fromMillisecondsSinceEpoch(scheduledAt.toInt() * 1000);
bool isBefore(DateTime date) => scheduledDate.isBefore(date);
extension _ReminderSort on Iterable<ReminderPB> {
List<ReminderPB> sortByScheduledAt() =>
sorted((a, b) => b.scheduledAt.compareTo(a.scheduledAt));
}
class NotificationDialog extends StatelessWidget {
class NotificationDialog extends StatefulWidget {
const NotificationDialog({
super.key,
required this.views,
@@ -29,94 +33,278 @@ class NotificationDialog extends StatelessWidget {
final List<ViewPB> views;
final PopoverMutex mutex;
@override
State<NotificationDialog> createState() => _NotificationDialogState();
}
class _NotificationDialogState extends State<NotificationDialog>
with SingleTickerProviderStateMixin {
late final TabController _controller = TabController(length: 2, vsync: this);
final PopoverMutex _mutex = PopoverMutex();
final ReminderBloc _reminderBloc = getIt<ReminderBloc>();
@override
void initState() {
super.initState();
_controller.addListener(_updateState);
}
void _updateState() => setState(() {});
@override
void dispose() {
_mutex.close();
_controller.removeListener(_updateState);
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final reminderBloc = getIt<ReminderBloc>();
return MultiBlocProvider(
providers: [
BlocProvider<ReminderBloc>.value(value: _reminderBloc),
BlocProvider<NotificationFilterBloc>(
create: (_) => NotificationFilterBloc(),
),
],
child: BlocBuilder<NotificationFilterBloc, NotificationFilterState>(
builder: (context, filterState) =>
BlocBuilder<ReminderBloc, ReminderState>(
builder: (context, state) {
final List<ReminderPB> pastReminders = state.pastReminders
.where((r) => filterState.showUnreadsOnly ? !r.isRead : true)
.sortByScheduledAt();
return BlocProvider<ReminderBloc>.value(
value: reminderBloc,
child: BlocBuilder<ReminderBloc, ReminderState>(
builder: (context, state) {
final shownReminders = state.reminders
.where((reminder) => reminder.isBefore(DateTime.now()))
.sorted((a, b) => b.scheduledAt.compareTo(a.scheduledAt));
final List<ReminderPB> upcomingReminders =
state.upcomingReminders.sortByScheduledAt();
return SingleChildScrollView(
child: Column(
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: DecoratedBox(
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
color: Theme.of(context).dividerColor,
),
),
),
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: 4,
horizontal: 10,
),
child: FlowyText.semibold(
LocaleKeys.notificationHub_title.tr(),
fontSize: 16,
),
const NotificationHubTitle(),
NotificationTabBar(tabController: _controller),
// TODO(Xazin): Resolve issue with taking up
// max amount of vertical space
Expanded(
child: TabBarView(
controller: _controller,
children: [
NotificationsView(
shownReminders: pastReminders,
reminderBloc: _reminderBloc,
views: widget.views,
onDelete: _onDelete,
onAction: _onAction,
onReadChanged: _onReadChanged,
actionBar: _InboxActionBar(
hasUnreads: state.hasUnreads,
showUnreadsOnly: filterState.showUnreadsOnly,
),
),
),
],
NotificationsView(
shownReminders: upcomingReminders,
reminderBloc: _reminderBloc,
views: widget.views,
isUpcoming: true,
onAction: _onAction,
),
],
),
),
const VSpace(4),
if (shownReminders.isEmpty)
Padding(
padding: const EdgeInsets.symmetric(vertical: 12),
child: Center(
child: FlowyText.regular(
LocaleKeys.notificationHub_empty.tr(),
),
),
)
else
...shownReminders.map((reminder) {
return NotificationItem(
reminderId: reminder.id,
key: ValueKey(reminder.id),
title: reminder.title,
scheduled: reminder.scheduledAt,
body: reminder.message,
isRead: reminder.isRead,
onReadChanged: (isRead) => reminderBloc.add(
ReminderEvent.update(
ReminderUpdate(id: reminder.id, isRead: isRead),
),
),
onDelete: () => reminderBloc
.add(ReminderEvent.remove(reminderId: reminder.id)),
onAction: () {
final view = views.firstWhereOrNull(
(view) => view.id == reminder.objectId,
);
if (view == null) {
return;
}
reminderBloc.add(
ReminderEvent.pressReminder(reminderId: reminder.id),
);
mutex.close();
},
);
}),
],
);
},
),
),
);
}
void _onAction(ReminderPB reminder) {
final view = widget.views.firstWhereOrNull(
(view) => view.id == reminder.objectId,
);
if (view == null) {
return;
}
_reminderBloc.add(
ReminderEvent.pressReminder(reminderId: reminder.id),
);
widget.mutex.close();
}
void _onDelete(ReminderPB reminder) {
_reminderBloc.add(ReminderEvent.remove(reminder: reminder));
}
void _onReadChanged(ReminderPB reminder, bool isRead) {
_reminderBloc.add(
ReminderEvent.update(ReminderUpdate(id: reminder.id, isRead: isRead)),
);
}
}
class _InboxActionBar extends StatelessWidget {
const _InboxActionBar({
required this.hasUnreads,
required this.showUnreadsOnly,
});
final bool hasUnreads;
final bool showUnreadsOnly;
@override
Widget build(BuildContext context) {
return DecoratedBox(
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
color: Theme.of(context).dividerColor,
),
),
),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
_MarkAsReadButton(
onMarkAllRead: !hasUnreads
? null
: () => context
.read<ReminderBloc>()
.add(const ReminderEvent.markAllRead()),
),
);
},
_ToggleUnreadsButton(
showUnreadsOnly: showUnreadsOnly,
onToggled: (showUnreadsOnly) => context
.read<NotificationFilterBloc>()
.add(const NotificationFilterEvent.toggleShowUnreadsOnly()),
),
],
),
),
);
}
}
class _ToggleUnreadsButton extends StatefulWidget {
const _ToggleUnreadsButton({
required this.onToggled,
this.showUnreadsOnly = false,
});
final Function(bool) onToggled;
final bool showUnreadsOnly;
@override
State<_ToggleUnreadsButton> createState() => _ToggleUnreadsButtonState();
}
class _ToggleUnreadsButtonState extends State<_ToggleUnreadsButton> {
late bool showUnreadsOnly = widget.showUnreadsOnly;
@override
Widget build(BuildContext context) {
return SegmentedButton<bool>(
onSelectionChanged: (Set<bool> newSelection) {
setState(() => showUnreadsOnly = newSelection.first);
widget.onToggled(showUnreadsOnly);
},
showSelectedIcon: false,
style: ButtonStyle(
side: MaterialStatePropertyAll(
BorderSide(color: Theme.of(context).dividerColor),
),
shape: const MaterialStatePropertyAll(
RoundedRectangleBorder(borderRadius: Corners.s6Border),
),
foregroundColor: MaterialStateProperty.resolveWith<Color>(
(state) {
if (state.contains(MaterialState.hovered) ||
state.contains(MaterialState.selected) ||
state.contains(MaterialState.pressed)) {
return Theme.of(context).colorScheme.onSurface;
}
return AFThemeExtension.of(context).textColor;
},
),
backgroundColor: MaterialStateProperty.resolveWith<Color>(
(state) {
if (state.contains(MaterialState.hovered) ||
state.contains(MaterialState.selected) ||
state.contains(MaterialState.pressed)) {
return Theme.of(context).colorScheme.primary;
}
return Theme.of(context).cardColor;
},
),
),
segments: [
ButtonSegment<bool>(
value: false,
label: Text(
LocaleKeys.notificationHub_actions_showAll.tr(),
style: const TextStyle(fontSize: 12),
),
),
ButtonSegment<bool>(
value: true,
label: Text(
LocaleKeys.notificationHub_actions_showUnreads.tr(),
style: const TextStyle(fontSize: 12),
),
),
],
selected: <bool>{showUnreadsOnly},
);
}
}
class _MarkAsReadButton extends StatefulWidget {
final VoidCallback? onMarkAllRead;
const _MarkAsReadButton({this.onMarkAllRead});
@override
State<_MarkAsReadButton> createState() => _MarkAsReadButtonState();
}
class _MarkAsReadButtonState extends State<_MarkAsReadButton> {
bool _isHovering = false;
@override
Widget build(BuildContext context) {
return Opacity(
opacity: widget.onMarkAllRead != null ? 1 : 0.5,
child: FlowyHover(
onHover: (isHovering) => setState(() => _isHovering = isHovering),
resetHoverOnRebuild: false,
child: FlowyTextButton(
LocaleKeys.notificationHub_actions_markAllRead.tr(),
fontColor: widget.onMarkAllRead != null && _isHovering
? Theme.of(context).colorScheme.onSurface
: AFThemeExtension.of(context).textColor,
heading: FlowySvg(
FlowySvgs.checklist_s,
color: widget.onMarkAllRead != null && _isHovering
? Theme.of(context).colorScheme.onSurface
: AFThemeExtension.of(context).textColor,
),
hoverColor: widget.onMarkAllRead != null && _isHovering
? Theme.of(context).colorScheme.primary
: null,
onPressed: widget.onMarkAllRead,
),
),
);
}
@@ -24,14 +24,16 @@ class NotificationButton extends StatelessWidget {
return BlocProvider<ReminderBloc>.value(
value: getIt<ReminderBloc>(),
child: BlocBuilder<ReminderBloc, ReminderState>(
builder: (context, state) => FlowyTooltip.delayed(
builder: (context, state) => FlowyTooltip(
message: LocaleKeys.notificationHub_title.tr(),
child: MouseRegion(
cursor: SystemMouseCursors.click,
child: AppFlowyPopover(
mutex: mutex,
direction: PopoverDirection.bottomWithLeftAligned,
constraints: const BoxConstraints(maxHeight: 250, maxWidth: 300),
constraints: const BoxConstraints(maxHeight: 250, maxWidth: 400),
windowPadding: EdgeInsets.zero,
margin: EdgeInsets.zero,
popupBuilder: (_) =>
NotificationDialog(views: views, mutex: mutex),
child: _buildNotificationIcon(context, state.hasUnreads),
@@ -0,0 +1,23 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flutter/material.dart';
class NotificationHubTitle extends StatelessWidget {
const NotificationHubTitle({
super.key,
});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16) +
const EdgeInsets.only(top: 12, bottom: 4),
child: FlowyText.semibold(
LocaleKeys.notificationHub_title.tr(),
color: Theme.of(context).colorScheme.tertiary,
fontSize: 16,
),
);
}
}
@@ -1,13 +1,14 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
import 'package:appflowy/workspace/application/settings/date_time/date_format_ext.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:fixnum/fixnum.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
DateFormat _dateFormat(BuildContext context) => DateFormat('MMM d, y');
import 'package:provider/provider.dart';
class NotificationItem extends StatefulWidget {
const NotificationItem({
@@ -17,6 +18,8 @@ class NotificationItem extends StatefulWidget {
required this.scheduled,
required this.body,
required this.isRead,
this.includeTime = false,
this.readOnly = false,
this.onAction,
this.onDelete,
this.onReadChanged,
@@ -26,6 +29,8 @@ class NotificationItem extends StatefulWidget {
final String title;
final Int64 scheduled;
final String body;
final bool includeTime;
final bool readOnly;
final bool isRead;
final VoidCallback? onAction;
@@ -53,64 +58,65 @@ class _NotificationItemState extends State<NotificationItem> {
GestureDetector(
onTap: widget.onAction,
child: Opacity(
opacity: widget.isRead ? 0.5 : 1,
child: Container(
padding: const EdgeInsets.all(10),
opacity: widget.isRead && !widget.readOnly ? 0.5 : 1,
child: DecoratedBox(
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(6)),
color: _isHovering && widget.onAction != null
? AFThemeExtension.of(context).lightGreyHover
: Colors.transparent,
border: widget.isRead || widget.readOnly
? null
: Border(
left: BorderSide(
width: 2,
color: Theme.of(context).colorScheme.primary,
),
),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Stack(
children: [
const FlowySvg(FlowySvgs.time_s, size: Size.square(20)),
if (!widget.isRead)
Positioned(
bottom: 1,
right: 1,
child: DecoratedBox(
decoration: BoxDecoration(
shape: BoxShape.circle,
color: AFThemeExtension.of(context).warning,
),
child: const SizedBox(height: 8, width: 8),
),
),
],
),
const HSpace(10),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(
child: FlowyText.semibold(widget.title),
),
FlowyText.regular(
_scheduledString(widget.scheduled),
fontSize: 10,
),
],
),
const VSpace(5),
FlowyText.regular(widget.body, maxLines: 4),
],
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: 10,
horizontal: 16,
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
FlowySvg(
FlowySvgs.time_s,
size: const Size.square(20),
color: Theme.of(context).colorScheme.tertiary,
),
),
],
const HSpace(16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
FlowyText.semibold(
widget.title,
fontSize: 14,
color: Theme.of(context).colorScheme.tertiary,
),
// TODO(Xazin): Relative time + View Name
FlowyText.regular(
_scheduledString(
widget.scheduled,
widget.includeTime,
),
fontSize: 10,
),
const VSpace(5),
FlowyText.regular(widget.body, maxLines: 4),
],
),
),
],
),
),
),
),
),
if (_isHovering)
if (_isHovering && !widget.readOnly)
Positioned(
right: 4,
top: 4,
@@ -125,10 +131,14 @@ class _NotificationItemState extends State<NotificationItem> {
);
}
String _scheduledString(Int64 secondsSinceEpoch) =>
_dateFormat(context).format(
DateTime.fromMillisecondsSinceEpoch(secondsSinceEpoch.toInt() * 1000),
);
String _scheduledString(Int64 secondsSinceEpoch, bool includeTime) {
final appearance = context.read<AppearanceSettingsCubit>().state;
return appearance.dateFormat.formatDate(
DateTime.fromMillisecondsSinceEpoch(secondsSinceEpoch.toInt() * 1000),
includeTime,
appearance.timeFormat,
);
}
void _onHover(bool isHovering) => setState(() => _isHovering = isHovering);
}
@@ -0,0 +1,78 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flutter/material.dart';
class NotificationTabBar extends StatelessWidget {
final TabController tabController;
const NotificationTabBar({
super.key,
required this.tabController,
});
@override
Widget build(BuildContext context) {
return DecoratedBox(
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
color: Theme.of(context).dividerColor,
),
),
),
child: Row(
children: [
Expanded(
child: TabBar(
controller: tabController,
padding: const EdgeInsets.symmetric(horizontal: 8),
labelPadding: EdgeInsets.zero,
indicatorSize: TabBarIndicatorSize.label,
indicator: UnderlineTabIndicator(
borderSide: BorderSide(
color: Theme.of(context).colorScheme.primary,
),
),
isScrollable: true,
tabs: [
_FlowyTab(
label: LocaleKeys.notificationHub_tabs_inbox.tr(),
isSelected: tabController.index == 0,
),
_FlowyTab(
label: LocaleKeys.notificationHub_tabs_upcoming.tr(),
isSelected: tabController.index == 1,
),
],
),
),
],
),
);
}
}
class _FlowyTab extends StatelessWidget {
final String label;
final bool isSelected;
const _FlowyTab({
required this.label,
required this.isSelected,
});
@override
Widget build(BuildContext context) {
return Tab(
height: 26,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: FlowyText.regular(
label,
color: isSelected ? Theme.of(context).colorScheme.tertiary : null,
),
),
);
}
}
@@ -0,0 +1,70 @@
import 'package:appflowy/user/application/reminder/reminder_extension.dart';
import 'package:appflowy/user/application/reminder/reminder_bloc.dart';
import 'package:appflowy/workspace/presentation/notifications/widgets/notification_item.dart';
import 'package:appflowy/workspace/presentation/notifications/widgets/notifications_hub_empty.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-user/reminder.pb.dart';
import 'package:flutter/material.dart';
class NotificationsView extends StatelessWidget {
const NotificationsView({
super.key,
required this.shownReminders,
required this.reminderBloc,
required this.views,
this.isUpcoming = false,
this.onAction,
this.onDelete,
this.onReadChanged,
this.actionBar,
});
final List<ReminderPB> shownReminders;
final ReminderBloc reminderBloc;
final List<ViewPB> views;
final bool isUpcoming;
final Function(ReminderPB reminder)? onAction;
final Function(ReminderPB reminder)? onDelete;
final Function(ReminderPB reminder, bool isRead)? onReadChanged;
final Widget? actionBar;
@override
Widget build(BuildContext context) {
if (shownReminders.isEmpty) {
return Column(
mainAxisSize: MainAxisSize.max,
children: [
if (actionBar != null) actionBar!,
const Expanded(child: NotificationsHubEmpty()),
],
);
}
return SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (actionBar != null) actionBar!,
...shownReminders.map(
(ReminderPB reminder) {
return NotificationItem(
reminderId: reminder.id,
key: ValueKey(reminder.id),
title: reminder.title,
scheduled: reminder.scheduledAt,
body: reminder.message,
isRead: reminder.isRead,
includeTime: reminder.includeTime ?? false,
readOnly: isUpcoming,
onReadChanged: (isRead) =>
onReadChanged?.call(reminder, isRead),
onDelete: () => onDelete?.call(reminder),
onAction: () => onAction?.call(reminder),
);
},
),
],
),
);
}
}
@@ -0,0 +1,32 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:flutter/material.dart';
class NotificationsHubEmpty extends StatelessWidget {
const NotificationsHubEmpty({super.key});
@override
Widget build(BuildContext context) {
return Center(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
FlowyText(
LocaleKeys.notificationHub_emptyTitle.tr(),
fontWeight: FontWeight.w700,
fontSize: 14,
),
const VSpace(8),
FlowyText.regular(
LocaleKeys.notificationHub_emptyBody.tr(),
),
],
),
),
);
}
}
@@ -1,5 +1,6 @@
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/settings_notifications_view.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/sync_setting_view.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/settings_appearance_view.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/settings_customize_shortcuts_view.dart';
@@ -101,6 +102,8 @@ class SettingsDialog extends StatelessWidget {
didLogout: didLogout,
didOpenUser: didOpenUser,
);
case SettingsPage.notifications:
return const SettingsNotificationsView();
case SettingsPage.syncSetting:
return SyncSettingView(userId: user.id.toString());
case SettingsPage.shortcuts:
@@ -36,12 +36,12 @@ void showEmojiPickerMenu(
final top = alignment == Alignment.topLeft ? offset.dy : null;
final bottom = alignment == Alignment.bottomLeft ? offset.dy : null;
keepEditorFocusNotifier.value += 1;
keepEditorFocusNotifier.increase();
final emojiPickerMenuEntry = FullScreenOverlayEntry(
top: top,
bottom: bottom,
left: offset.dx,
dismissCallback: () => keepEditorFocusNotifier.value -= 1,
dismissCallback: () => keepEditorFocusNotifier.decrease(),
builder: (context) => Material(
type: MaterialType.transparency,
child: Container(
@@ -2,7 +2,7 @@ import 'dart:io';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/workspace/application/appearance.dart';
import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
@@ -1,6 +1,6 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/workspace/application/appearance.dart';
import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
import 'package:appflowy/workspace/presentation/home/toast.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/settings_appearance/theme_setting_entry_template.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/theme_upload/theme_upload_view.dart';
@@ -1,6 +1,6 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/workspace/application/appearance.dart';
import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
import 'package:appflowy_backend/protobuf/flowy-user/date_time.pbenum.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
@@ -1,7 +1,7 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/more/cubit/document_appearance_cubit.dart';
import 'package:appflowy/workspace/application/appearance.dart';
import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
@@ -2,7 +2,7 @@ import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/more/cubit/document_appearance_cubit.dart';
import 'package:appflowy/util/google_font_family_extension.dart';
import 'package:appflowy/workspace/application/appearance.dart';
import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
import 'package:appflowy/workspace/application/appearance_defaults.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:collection/collection.dart';
@@ -1,6 +1,6 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/workspace/application/appearance.dart';
import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
import 'package:appflowy_backend/protobuf/flowy-user/date_time.pbenum.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
@@ -1,4 +1,4 @@
import 'package:appflowy/workspace/application/appearance.dart';
import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/settings_appearance/create_file_setting.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/settings_appearance/date_format_setting.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/settings_appearance/time_format_setting.dart';
Loaded 100 of 278 files, more files were not shown because too many files have changed in this diff. Show more