From 79585d0835af99847a213b1c7d5d74d0702ed5a2 Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Sat, 2 Mar 2024 17:30:27 +0800 Subject: [PATCH 1/8] fix: flutter_ci (#4793) --- .github/workflows/flutter_ci.yaml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/.github/workflows/flutter_ci.yaml b/.github/workflows/flutter_ci.yaml index 5c9b76ff9e..d3ddc17be8 100644 --- a/.github/workflows/flutter_ci.yaml +++ b/.github/workflows/flutter_ci.yaml @@ -72,12 +72,20 @@ jobs: override: true profile: minimal + - name: Export pub environment variables and add to PATH + run: | + if [ "$RUNNER_OS" == "Windows" ]; then + echo "PUB_CACHE=$LOCALAPPDATA\\Pub\\Cache" >> $GITHUB_ENV + fi + shell: bash + - name: Install flutter id: flutter uses: subosito/flutter-action@v2 with: channel: "stable" flutter-version: ${{ env.FLUTTER_VERSION }} + cache: true - uses: Swatinem/rust-cache@v2 with: @@ -159,6 +167,7 @@ jobs: with: channel: "stable" flutter-version: ${{ env.FLUTTER_VERSION }} + cache: true - uses: Swatinem/rust-cache@v2 with: @@ -262,6 +271,7 @@ jobs: with: channel: "stable" flutter-version: ${{ env.FLUTTER_VERSION }} + cache: true - uses: taiki-e/install-action@v2 with: @@ -336,6 +346,7 @@ jobs: with: channel: "stable" flutter-version: ${{ env.FLUTTER_VERSION }} + cache: true - uses: taiki-e/install-action@v2 with: @@ -427,6 +438,7 @@ jobs: with: channel: "stable" flutter-version: ${{ env.FLUTTER_VERSION }} + cache: true - uses: taiki-e/install-action@v2 with: From f4170755fa5569485b902b7bccdf2901de37f3a0 Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Sat, 2 Mar 2024 18:27:34 +0800 Subject: [PATCH 2/8] fix: reorder grid field updating all views (#4791) --- .../src/services/database/database_editor.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs b/frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs index acd99968b0..7e79d74291 100644 --- a/frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs +++ b/frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs @@ -594,13 +594,15 @@ impl DatabaseEditor { index: index as i32, }; let notified_changeset = DatabaseFieldChangesetPB { - view_id: params.view_id, + view_id: params.view_id.clone(), inserted_fields: vec![insert_field], deleted_fields: vec![delete_field], updated_fields: vec![], }; - self.notify_did_update_database(notified_changeset).await?; + send_notification(¶ms.view_id, DatabaseNotification::DidUpdateFields) + .payload(notified_changeset) + .send(); } Ok(()) From b38fc4330088572a3ef7adab4332cac64cf2f12a Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Sat, 2 Mar 2024 19:14:00 +0800 Subject: [PATCH 3/8] feat: checklist cell editor a11y improvement (#4784) --- .../cell/bloc/checklist_cell_bloc.dart | 50 ++--- .../cell/relation_cell_service.dart | 0 .../checklist_card_cell.dart | 2 +- .../desktop_row_detail_checklist_cell.dart | 23 +- .../editable_cell_skeleton/checklist.dart | 2 +- .../cell_editor/checklist_cell_editor.dart | 196 +++++++++++------- .../mobile_checklist_cell_editor.dart | 4 +- .../appflowy_popover/lib/src/popover.dart | 17 +- 8 files changed, 169 insertions(+), 125 deletions(-) delete mode 100644 frontend/appflowy_flutter/lib/plugins/database/application/cell/relation_cell_service.dart diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/checklist_cell_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/checklist_cell_bloc.dart index 756a3dafb5..7fe92cd137 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/checklist_cell_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/checklist_cell_bloc.dart @@ -11,7 +11,7 @@ import 'package:freezed_annotation/freezed_annotation.dart'; part 'checklist_cell_bloc.freezed.dart'; class ChecklistSelectOption { - ChecklistSelectOption(this.isSelected, this.data); + ChecklistSelectOption({required this.isSelected, required this.data}); final bool isSelected; final SelectOptionPB data; @@ -26,6 +26,7 @@ class ChecklistCellBloc extends Bloc { ), super(ChecklistCellState.initial(cellController)) { _dispatch(); + _startListening(); } final ChecklistCellController cellController; @@ -46,9 +47,6 @@ class ChecklistCellBloc extends Bloc { on( (event, emit) async { await event.when( - initial: () { - _startListening(); - }, didReceiveOptions: (data) { if (data == null) { emit( @@ -71,8 +69,8 @@ class ChecklistCellBloc extends Bloc { updateTaskName: (option, name) { _updateOption(option, name); }, - selectTask: (option) async { - await _checklistCellService.select(optionId: option.id); + selectTask: (id) async { + await _checklistCellService.select(optionId: id); }, createNewTask: (name) async { final result = await _checklistCellService.create(name: name); @@ -81,8 +79,8 @@ class ChecklistCellBloc extends Bloc { (err) => Log.error(err), ); }, - deleteTask: (option) async { - await _deleteOption([option]); + deleteTask: (id) async { + await _deleteOption([id]); }, ); }, @@ -102,21 +100,17 @@ class ChecklistCellBloc extends Bloc { void _updateOption(SelectOptionPB option, String name) async { final result = await _checklistCellService.updateName(option: option, name: name); - result.fold((l) => null, (err) => Log.error(err)); } - Future _deleteOption(List options) async { - final result = await _checklistCellService.delete( - optionIds: options.map((e) => e.id).toList(), - ); + Future _deleteOption(List options) async { + final result = await _checklistCellService.delete(optionIds: options); result.fold((l) => null, (err) => Log.error(err)); } } @freezed class ChecklistCellEvent with _$ChecklistCellEvent { - const factory ChecklistCellEvent.initial() = _InitialCell; const factory ChecklistCellEvent.didReceiveOptions( ChecklistCellDataPB? data, ) = _DidReceiveCellUpdate; @@ -124,12 +118,10 @@ class ChecklistCellEvent with _$ChecklistCellEvent { SelectOptionPB option, String name, ) = _UpdateTaskName; - const factory ChecklistCellEvent.selectTask(SelectOptionPB task) = - _SelectTask; + const factory ChecklistCellEvent.selectTask(String taskId) = _SelectTask; const factory ChecklistCellEvent.createNewTask(String description) = _CreateNewTask; - const factory ChecklistCellEvent.deleteTask(SelectOptionPB option) = - _DeleteTask; + const factory ChecklistCellEvent.deleteTask(String taskId) = _DeleteTask; } @freezed @@ -157,16 +149,14 @@ List _makeChecklistSelectOptions( if (data == null) { return []; } - - final List options = []; - final List allOptions = List.from(data.options); - final selectedOptionIds = data.selectedOptions.map((e) => e.id).toList(); - - for (final option in allOptions) { - options.add( - ChecklistSelectOption(selectedOptionIds.contains(option.id), option), - ); - } - - return options; + return data.options + .map( + (option) => ChecklistSelectOption( + isSelected: data.selectedOptions.any( + (selected) => selected.id == option.id, + ), + data: option, + ), + ) + .toList(); } diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/cell/relation_cell_service.dart b/frontend/appflowy_flutter/lib/plugins/database/application/cell/relation_cell_service.dart deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_skeleton/checklist_card_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_skeleton/checklist_card_cell.dart index 63cd450cff..a45b621dde 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_skeleton/checklist_card_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_skeleton/checklist_card_cell.dart @@ -42,7 +42,7 @@ class _ChecklistCellState extends State { widget.databaseController, widget.cellContext, ).as(), - )..add(const ChecklistCellEvent.initial()); + ); }, child: BlocBuilder( builder: (context, state) { diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_checklist_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_checklist_cell.dart index 3c4576e4c3..69d7c07c09 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_checklist_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_checklist_cell.dart @@ -64,14 +64,17 @@ class _ChecklistItemsState extends State { } final children = tasks .mapIndexed( - (index, task) => ChecklistItem( - task: task, - autofocus: widget.state.newTask && index == tasks.length - 1, - onSubmitted: () { - if (index == tasks.length - 1) { - widget.bloc.add(const ChecklistCellEvent.createNewTask("")); - } - }, + (index, task) => Padding( + padding: const EdgeInsets.symmetric(vertical: 2.0), + child: ChecklistItem( + task: task, + autofocus: widget.state.newTask && index == tasks.length - 1, + onSubmitted: () { + if (index == tasks.length - 1) { + widget.bloc.add(const ChecklistCellEvent.createNewTask("")); + } + }, + ), ), ) .toList(); @@ -111,7 +114,7 @@ class _ChecklistItemsState extends State { ], ), ), - const VSpace(4), + const VSpace(2.0), ...children, ChecklistItemControl(cellNotifer: widget.cellContainerNotifier), ], @@ -136,7 +139,7 @@ class ChecklistItemControl extends StatelessWidget { .read() .add(const ChecklistCellEvent.createNewTask("")), child: Container( - margin: const EdgeInsets.fromLTRB(8.0, 4.0, 8.0, 0), + margin: const EdgeInsets.fromLTRB(8.0, 2.0, 8.0, 0), height: 12, child: AnimatedSwitcher( duration: const Duration(milliseconds: 150), diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/checklist.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/checklist.dart index 2f3407aea6..dd7bc6c2c1 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/checklist.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/checklist.dart @@ -58,7 +58,7 @@ class GridChecklistCellState extends GridCellState { widget.databaseController, widget.cellContext, ).as(), - )..add(const ChecklistCellEvent.initial()); + ); @override void dispose() { diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/checklist_cell_editor.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/checklist_cell_editor.dart index a2d0c4adb6..f985e26560 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/checklist_cell_editor.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/checklist_cell_editor.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:io'; import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; @@ -23,10 +24,10 @@ class ChecklistCellEditor extends StatefulWidget { final ChecklistCellController cellController; @override - State createState() => _GridChecklistCellState(); + State createState() => _ChecklistCellEditorState(); } -class _GridChecklistCellState extends State { +class _ChecklistCellEditorState extends State { /// Focus node for the new task text field late final FocusNode newTaskFocusNode; @@ -56,18 +57,14 @@ class _GridChecklistCellState extends State { return Column( mainAxisSize: MainAxisSize.min, children: [ - AnimatedSwitcher( - duration: const Duration(milliseconds: 300), - child: state.tasks.isEmpty - ? const SizedBox.shrink() - : Padding( - padding: const EdgeInsets.fromLTRB(16, 16, 16, 4), - child: ChecklistProgressBar( - tasks: state.tasks, - percent: state.percent, - ), - ), - ), + if (state.tasks.isNotEmpty) + Padding( + padding: const EdgeInsets.fromLTRB(16, 16, 16, 4), + child: ChecklistProgressBar( + tasks: state.tasks, + percent: state.percent, + ), + ), ChecklistItemList( options: state.tasks, onUpdateTask: () => newTaskFocusNode.requestFocus(), @@ -92,7 +89,7 @@ class _GridChecklistCellState extends State { /// Displays the a list of all the exisiting tasks and an input field to create /// a new task if `isAddingNewTask` is true -class ChecklistItemList extends StatefulWidget { +class ChecklistItemList extends StatelessWidget { const ChecklistItemList({ super.key, required this.options, @@ -102,26 +99,19 @@ class ChecklistItemList extends StatefulWidget { final List options; final VoidCallback onUpdateTask; - @override - State createState() => _ChecklistItemListState(); -} - -class _ChecklistItemListState extends State { @override Widget build(BuildContext context) { - if (widget.options.isEmpty) { + if (options.isEmpty) { return const SizedBox.shrink(); } - final itemList = widget.options + final itemList = options .mapIndexed( (index, option) => Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0), child: ChecklistItem( task: option, - onSubmitted: index == widget.options.length - 1 - ? widget.onUpdateTask - : null, + onSubmitted: index == options.length - 1 ? onUpdateTask : null, key: ValueKey(option.data.id), ), ), @@ -140,6 +130,22 @@ class _ChecklistItemListState extends State { } } +class _SelectTaskIntent extends Intent { + const _SelectTaskIntent(); +} + +class _DeleteTaskIntent extends Intent { + const _DeleteTaskIntent(); +} + +class _StartEditingTaskIntent extends Intent { + const _StartEditingTaskIntent(); +} + +class _EndEditingTaskIntent extends Intent { + const _EndEditingTaskIntent(); +} + /// Represents an existing task @visibleForTesting class ChecklistItem extends StatefulWidget { @@ -160,58 +166,80 @@ class ChecklistItem extends StatefulWidget { class _ChecklistItemState extends State { late final TextEditingController _textController; - late final FocusNode _focusNode; + final FocusNode _focusNode = FocusNode(); bool _isHovered = false; + bool _isFocused = false; Timer? _debounceOnChanged; @override void initState() { super.initState(); _textController = TextEditingController(text: widget.task.data.name); - _focusNode = FocusNode( - onKeyEvent: (node, event) { - if (event.logicalKey == LogicalKeyboardKey.escape) { - node.unfocus(); - return KeyEventResult.handled; - } - return KeyEventResult.ignored; - }, - ); - if (widget.autofocus) { - _focusNode.requestFocus(); - } } @override void dispose() { + _debounceOnChanged?.cancel(); _textController.dispose(); _focusNode.dispose(); - _debounceOnChanged?.cancel(); super.dispose(); } @override void didUpdateWidget(ChecklistItem oldWidget) { super.didUpdateWidget(oldWidget); - if (widget.task.data.name != oldWidget.task.data.name && - !_focusNode.hasFocus) { + if (widget.task.data.name != oldWidget.task.data.name) { _textController.text = widget.task.data.name; } } @override Widget build(BuildContext context) { - final icon = FlowySvg( - widget.task.isSelected ? FlowySvgs.check_filled_s : FlowySvgs.uncheck_s, - blendMode: BlendMode.dst, - ); - return MouseRegion( - onEnter: (event) => setState(() => _isHovered = true), - onExit: (event) => setState(() => _isHovered = false), + return FocusableActionDetector( + onShowHoverHighlight: (isHovered) { + setState(() => _isHovered = isHovered); + }, + onFocusChange: (isFocused) { + setState(() => _isFocused = isFocused); + }, + actions: { + _SelectTaskIntent: CallbackAction<_SelectTaskIntent>( + onInvoke: (_SelectTaskIntent intent) => context + .read() + .add(ChecklistCellEvent.selectTask(widget.task.data.id)), + ), + _DeleteTaskIntent: CallbackAction<_DeleteTaskIntent>( + onInvoke: (_DeleteTaskIntent intent) => context + .read() + .add(ChecklistCellEvent.deleteTask(widget.task.data.id)), + ), + _StartEditingTaskIntent: CallbackAction<_StartEditingTaskIntent>( + onInvoke: (_StartEditingTaskIntent intent) => + _focusNode.requestFocus(), + ), + _EndEditingTaskIntent: CallbackAction<_EndEditingTaskIntent>( + onInvoke: (_EndEditingTaskIntent intent) => _focusNode.unfocus(), + ), + }, + shortcuts: { + const SingleActivator(LogicalKeyboardKey.space): + const _SelectTaskIntent(), + const SingleActivator(LogicalKeyboardKey.delete): + const _DeleteTaskIntent(), + const SingleActivator(LogicalKeyboardKey.enter): + const _StartEditingTaskIntent(), + if (Platform.isMacOS) + const SingleActivator(LogicalKeyboardKey.enter, meta: true): + const _SelectTaskIntent() + else + const SingleActivator(LogicalKeyboardKey.enter, control: true): + const _SelectTaskIntent(), + }, + descendantsAreTraversable: false, child: Container( constraints: BoxConstraints(minHeight: GridSize.popoverItemHeight), decoration: BoxDecoration( - color: _isHovered + color: _isHovered || _isFocused || _focusNode.hasFocus ? AFThemeExtension.of(context).lightGreyHover : Colors.transparent, borderRadius: Corners.s6Border, @@ -220,43 +248,65 @@ class _ChecklistItemState extends State { children: [ FlowyIconButton( width: 32, - icon: icon, + icon: FlowySvg( + widget.task.isSelected + ? FlowySvgs.check_filled_s + : FlowySvgs.uncheck_s, + blendMode: BlendMode.dst, + ), hoverColor: Colors.transparent, onPressed: () => context.read().add( - ChecklistCellEvent.selectTask(widget.task.data), + ChecklistCellEvent.selectTask(widget.task.data.id), ), ), Expanded( - child: TextField( - controller: _textController, - focusNode: _focusNode, - style: Theme.of(context).textTheme.bodyMedium, - decoration: InputDecoration( - border: InputBorder.none, - isCollapsed: true, - contentPadding: EdgeInsets.only( - top: 8.0, - bottom: 8.0, - left: 2.0, - right: _isHovered ? 2.0 : 8.0, - ), - hintText: LocaleKeys.grid_checklist_taskHint.tr(), - ), - onChanged: _debounceOnChangedText, - onSubmitted: (description) { - _submitUpdateTaskDescription(description); - widget.onSubmitted?.call(); + child: Shortcuts( + shortcuts: const { + SingleActivator(LogicalKeyboardKey.space): + DoNothingAndStopPropagationIntent(), + SingleActivator(LogicalKeyboardKey.delete): + DoNothingAndStopPropagationIntent(), + SingleActivator(LogicalKeyboardKey.enter): + DoNothingAndStopPropagationIntent(), + SingleActivator(LogicalKeyboardKey.escape): + _EndEditingTaskIntent(), }, + child: TextField( + controller: _textController, + focusNode: _focusNode, + autofocus: widget.autofocus, + style: Theme.of(context).textTheme.bodyMedium, + decoration: InputDecoration( + border: InputBorder.none, + isCollapsed: true, + contentPadding: EdgeInsets.only( + top: 8.0, + bottom: 8.0, + left: 2.0, + right: _isHovered ? 2.0 : 8.0, + ), + hintText: LocaleKeys.grid_checklist_taskHint.tr(), + ), + onChanged: (text) { + if (_textController.value.composing.isCollapsed) { + _debounceOnChangedText(text); + } + }, + onSubmitted: (description) { + _submitUpdateTaskDescription(description); + widget.onSubmitted?.call(); + }, + ), ), ), - if (_isHovered) + if (_isHovered || _isFocused || _focusNode.hasFocus) FlowyIconButton( width: 32, icon: const FlowySvg(FlowySvgs.delete_s), hoverColor: Colors.transparent, iconColorOnHover: Theme.of(context).colorScheme.error, onPressed: () => context.read().add( - ChecklistCellEvent.deleteTask(widget.task.data), + ChecklistCellEvent.deleteTask(widget.task.data.id), ), ), ], @@ -276,7 +326,7 @@ class _ChecklistItemState extends State { context.read().add( ChecklistCellEvent.updateTaskName( widget.task.data, - description.trim(), + description, ), ); } diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/mobile_checklist_cell_editor.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/mobile_checklist_cell_editor.dart index 944d438e69..f775896aec 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/mobile_checklist_cell_editor.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/mobile_checklist_cell_editor.dart @@ -159,7 +159,7 @@ class _ChecklistItemState extends State<_ChecklistItem> { borderRadius: BorderRadius.circular(22), onTap: () => context .read() - .add(ChecklistCellEvent.selectTask(widget.task.data)), + .add(ChecklistCellEvent.selectTask(widget.task.data.id)), child: SizedBox.square( dimension: 44, child: Center( @@ -239,7 +239,7 @@ class _ChecklistItemState extends State<_ChecklistItem> { child: InkWell( onTap: () { context.read().add( - ChecklistCellEvent.deleteTask(widget.task.data), + ChecklistCellEvent.deleteTask(widget.task.data.id), ); context.pop(); }, diff --git a/frontend/appflowy_flutter/packages/appflowy_popover/lib/src/popover.dart b/frontend/appflowy_flutter/packages/appflowy_popover/lib/src/popover.dart index 8a18b447a7..4c93b40e78 100644 --- a/frontend/appflowy_flutter/packages/appflowy_popover/lib/src/popover.dart +++ b/frontend/appflowy_flutter/packages/appflowy_popover/lib/src/popover.dart @@ -161,15 +161,16 @@ class PopoverState extends State { ), ); - return FocusScope( - onKey: (node, event) { - if (event.logicalKey == LogicalKeyboardKey.escape) { - _removeRootOverlay(); - return KeyEventResult.handled; - } - return KeyEventResult.ignored; + return CallbackShortcuts( + bindings: { + const SingleActivator(LogicalKeyboardKey.escape): () => + _removeRootOverlay(), }, - child: Stack(children: children), + child: FocusScope( + child: Stack( + children: children, + ), + ), ); }); _rootEntry.addEntry(context, this, newEntry, widget.asBarrier); From 8c7ed808a795eed80763a9679b2b086f937825ee Mon Sep 17 00:00:00 2001 From: Sore <88344148+SoranTabesh@users.noreply.github.com> Date: Sat, 2 Mar 2024 18:21:20 +0330 Subject: [PATCH 4/8] chore: rename ckb-KUR.json to ckb-KU.json (#4786) --- frontend/resources/translations/{ckb-KUR.json => ckb-KU.json} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename frontend/resources/translations/{ckb-KUR.json => ckb-KU.json} (100%) diff --git a/frontend/resources/translations/ckb-KUR.json b/frontend/resources/translations/ckb-KU.json similarity index 100% rename from frontend/resources/translations/ckb-KUR.json rename to frontend/resources/translations/ckb-KU.json From f7233f6949fe22e028b07c73cf751b4a7d4ab1fb Mon Sep 17 00:00:00 2001 From: YiHeCN <102681555+YiHeCN@users.noreply.github.com> Date: Sun, 3 Mar 2024 00:05:43 +0800 Subject: [PATCH 5/8] chore: update zh-CN translations (#4795) --- frontend/resources/translations/ar-SA.json | 4 +- frontend/resources/translations/de-DE.json | 4 +- frontend/resources/translations/es-VE.json | 4 +- frontend/resources/translations/fr-CA.json | 4 +- frontend/resources/translations/fr-FR.json | 4 +- frontend/resources/translations/it-IT.json | 8 +-- frontend/resources/translations/ru-RU.json | 4 +- frontend/resources/translations/zh-CN.json | 62 +++++++++++++++++++--- frontend/resources/translations/zh-TW.json | 4 +- 9 files changed, 73 insertions(+), 25 deletions(-) diff --git a/frontend/resources/translations/ar-SA.json b/frontend/resources/translations/ar-SA.json index 96231762aa..fae5ea6aa9 100644 --- a/frontend/resources/translations/ar-SA.json +++ b/frontend/resources/translations/ar-SA.json @@ -848,10 +848,10 @@ "addToColumnBottomTooltip": "أضف بطاقة جديدة في الأسفل", "renameColumn": "إعادة تسمية", "hideColumn": "اخفاء", - "groupActions": "إجراءات المجموعة", "newGroup": "مجموعة جديدة", "deleteColumn": "مسح", - "deleteColumnConfirmation": "سيؤدي هذا إلى حذف هذه المجموعة وجميع البطاقات الموجودة فيها. هل أنت متأكد من الاستمرار؟" + "deleteColumnConfirmation": "سيؤدي هذا إلى حذف هذه المجموعة وجميع البطاقات الموجودة فيها. هل أنت متأكد من الاستمرار؟", + "groupActions": "إجراءات المجموعة" }, "hiddenGroupSection": { "sectionTitle": "المجموعات المخفية", diff --git a/frontend/resources/translations/de-DE.json b/frontend/resources/translations/de-DE.json index ca644f9bf2..0e9305c744 100644 --- a/frontend/resources/translations/de-DE.json +++ b/frontend/resources/translations/de-DE.json @@ -868,10 +868,10 @@ "addToColumnBottomTooltip": "Eine neue Karte am unteren Ende hinzufügen", "renameColumn": "Umbenennen", "hideColumn": "Verstecken", - "groupActions": "Gruppenaktion", "newGroup": "Neue Gruppe", "deleteColumn": "Löschen", - "deleteColumnConfirmation": "Das wird diese Gruppe und alle enthaltenen Karten löschen.\nSicher, dass Sie fortsetzen möchte?" + "deleteColumnConfirmation": "Das wird diese Gruppe und alle enthaltenen Karten löschen.\nSicher, dass Sie fortsetzen möchte?", + "groupActions": "Gruppenaktion" }, "hiddenGroupSection": { "sectionTitle": "Versteckte Gruppen", diff --git a/frontend/resources/translations/es-VE.json b/frontend/resources/translations/es-VE.json index 69c420ac7f..e4f79f4a7d 100644 --- a/frontend/resources/translations/es-VE.json +++ b/frontend/resources/translations/es-VE.json @@ -834,9 +834,9 @@ "createNewColumn": "Agregar un nuevo grupo", "renameColumn": "Renombrar", "hideColumn": "Ocultar", - "groupActions": "Acciones grupales", "newGroup": "Nuevo grupo", - "deleteColumn": "Borrar" + "deleteColumn": "Borrar", + "groupActions": "Acciones grupales" }, "hiddenGroupSection": { "sectionTitle": "Grupos ocultos" diff --git a/frontend/resources/translations/fr-CA.json b/frontend/resources/translations/fr-CA.json index 9397a95599..ffd284cad7 100644 --- a/frontend/resources/translations/fr-CA.json +++ b/frontend/resources/translations/fr-CA.json @@ -903,10 +903,10 @@ "addToColumnBottomTooltip": "Ajouter une nouvelle carte en bas", "renameColumn": "Renommer", "hideColumn": "Cacher", - "groupActions": "Actions de groupe", "newGroup": "Nouveau groupe", "deleteColumn": "Supprimer", - "deleteColumnConfirmation": "Cela supprimera ce groupe et toutes les cartes qu'il contient. \nÊtes-vous sûr de vouloir continuer?" + "deleteColumnConfirmation": "Cela supprimera ce groupe et toutes les cartes qu'il contient. \nÊtes-vous sûr de vouloir continuer?", + "groupActions": "Actions de groupe" }, "hiddenGroupSection": { "sectionTitle": "Groupes cachés", diff --git a/frontend/resources/translations/fr-FR.json b/frontend/resources/translations/fr-FR.json index a2d14e4bcf..8d5e9111cd 100644 --- a/frontend/resources/translations/fr-FR.json +++ b/frontend/resources/translations/fr-FR.json @@ -900,10 +900,10 @@ "addToColumnBottomTooltip": "Ajouter une nouvelle carte en bas", "renameColumn": "Renommer", "hideColumn": "Cacher", - "groupActions": "Actions de groupe", "newGroup": "Nouveau groupe", "deleteColumn": "Supprimer", - "deleteColumnConfirmation": "Cela supprimera ce groupe et toutes les cartes qu'il contient. \nÊtes-vous sûr de vouloir continuer?" + "deleteColumnConfirmation": "Cela supprimera ce groupe et toutes les cartes qu'il contient. \nÊtes-vous sûr de vouloir continuer?", + "groupActions": "Actions de groupe" }, "hiddenGroupSection": { "sectionTitle": "Groupes cachés", diff --git a/frontend/resources/translations/it-IT.json b/frontend/resources/translations/it-IT.json index 60f0d80426..ad7a91add2 100644 --- a/frontend/resources/translations/it-IT.json +++ b/frontend/resources/translations/it-IT.json @@ -483,14 +483,14 @@ "typeAValue": "Digita un valore...", "layout": "Disposizione", "databaseLayout": "Disposizione", - "viewList": "Viste del database", "editView": "Modifica vista", "boardSettings": "Impostazioni della bacheca", "calendarSettings": "Impostazioni del calendario", "createView": "Nuova vista", "duplicateView": "Duplica vista", "deleteView": "Elimina vista", - "numberOfVisibleFields": "{} mostrato" + "numberOfVisibleFields": "{} mostrato", + "viewList": "Viste del database" }, "textFilter": { "contains": "Contiene", @@ -964,10 +964,10 @@ "layoutDateField": "Layout calendario per", "changeLayoutDateField": "Modifica del campo di layout", "noDateTitle": "Nessuna data", - "noDateHint": "Gli eventi non programmati verranno visualizzati qui", "unscheduledEventsTitle": "Eventi non programmati", "clickToAdd": "Fare clic per aggiungere al calendario", - "name": "Disposizione del calendario" + "name": "Disposizione del calendario", + "noDateHint": "Gli eventi non programmati verranno visualizzati qui" }, "referencedCalendarPrefix": "Vista di", "quickJumpYear": "Salta a" diff --git a/frontend/resources/translations/ru-RU.json b/frontend/resources/translations/ru-RU.json index e5aaf01816..b7dae6282e 100644 --- a/frontend/resources/translations/ru-RU.json +++ b/frontend/resources/translations/ru-RU.json @@ -870,10 +870,10 @@ "addToColumnBottomTooltip": "Add a new card at the bottom", "renameColumn": "Переименовать", "hideColumn": "Скрыть", - "groupActions": "Групповые действия", "newGroup": "Новая группа", "deleteColumn": "Удалить", - "deleteColumnConfirmation": "Это удалит колонку и все карточки в ней.\nТочно продолжить?" + "deleteColumnConfirmation": "Это удалит колонку и все карточки в ней.\nТочно продолжить?", + "groupActions": "Групповые действия" }, "hiddenGroupSection": { "sectionTitle": "Скрытые группы", diff --git a/frontend/resources/translations/zh-CN.json b/frontend/resources/translations/zh-CN.json index e4a822fd98..e2bc243762 100644 --- a/frontend/resources/translations/zh-CN.json +++ b/frontend/resources/translations/zh-CN.json @@ -79,7 +79,12 @@ "large": "大", "fontSize": "字体大小", "import": "导入", - "moreOptions": "更多选项" + "moreOptions": "更多选项", + "wordCount": "字数: {}", + "charCount": "字符数:{}", + "createdAt": "创建于:{}", + "deleteView": "删除", + "duplicateView": "复制" }, "importPanel": { "textAndMarkdown": "文本和Markdown", @@ -236,7 +241,14 @@ "rename": "重命名", "helpCenter": "帮助中心", "add": "添加", - "yes": "是" + "yes": "是", + "Done": "完成", + "Cancel": "取消", + "clear": "清空", + "remove": "删除", + "dontRemove": "请勿删除", + "copyLink": "复制链接", + "align": "对齐" }, "label": { "welcome": "欢迎!", @@ -281,10 +293,12 @@ "cloudLocal": "本地", "cloudSupabase": "Supabase", "cloudSupabaseUrl": "Supabase URL", + "cloudSupabaseUrlCanNotBeEmpty": "SUPABASE 地址不能为空", "cloudSupabaseAnonKey": "Supabase anon key", "cloudSupabaseAnonKeyCanNotBeEmpty": "如果 Supabase url 不为空,则 Anon key 不能为空", "cloudAppFlowy": "AppFlowy云", "cloudAppFlowySelfHost": "AppFlowy 云自行托管", + "appFlowyCloudUrlCanNotBeEmpty": "云地址不能为空", "clickToCopy": "点击复制", "selfHostStart": "如果您没有服务器,请参阅", "selfHostContent": "文档", @@ -332,6 +346,7 @@ "dark": "深色", "system": "跟随系统" }, + "fontScaleFactor": "字体缩放比例", "documentSettings": { "cursorColor": "光标颜色", "selectionColor": "文本选择颜色", @@ -540,7 +555,25 @@ "onOrAfter": "在或之后", "between": "之间", "empty": "为空", - "notEmpty": "不为空" + "notEmpty": "不为空", + "choicechipPrefix": { + "before": "之前", + "after": "之后", + "onOrBefore": "当天或之前", + "onOrAfter": "当天或之后", + "isEmpty": "为空", + "isNotEmpty": "不为空" + } + }, + "numberFilter": { + "equal": "等于", + "notEqual": "不等于", + "lessThan": "小于", + "greaterThan": "大于", + "lessThanOrEqualTo": "小于等于", + "greaterThanOrEqualTo": "大于等于", + "isEmpty": "为空", + "isNotEmpty": "不为空" }, "field": { "hide": "隐藏", @@ -559,6 +592,7 @@ "multiSelectFieldName": "多选", "urlFieldName": "网址", "checklistFieldName": "清单", + "relationFieldName": "关系", "numberFormat": "数字格式", "dateFormat": "日期格式", "includeTime": "包含时/分", @@ -590,7 +624,8 @@ "deleteFieldPromptMessage": "确定要删除这个属性吗? ", "newColumn": "新建列", "format": "格式", - "reminderOnDateTooltip": "这个单元格设置了一个预定的提醒" + "reminderOnDateTooltip": "这个单元格设置了一个预定的提醒", + "optionAlreadyExist": "选项已存在" }, "rowPage": { "newField": "添加新字段", @@ -657,8 +692,21 @@ "hideComplete": "隐藏已完成的任务", "showComplete": "显示所有任务" }, + "relation": { + "emptySearchResult": "无结果" + }, "menuName": "网格", - "referencedGridPrefix": "视图" + "referencedGridPrefix": "视图", + "calculate": "计算", + "calculationTypeLabel": { + "none": "无", + "average": "均值", + "max": "最大值", + "median": "中位数", + "min": "最小值", + "sum": "求和" + }, + "removeSorting": "你确定要移除排序吗?" }, "document": { "menuName": "文档", @@ -888,10 +936,10 @@ "addToColumnBottomTooltip": "在底部添加一张新卡片", "renameColumn": "改名", "hideColumn": "隐藏", - "groupActions": "组操作", "newGroup": "新建组", "deleteColumn": "删除", - "deleteColumnConfirmation": "这将删除该组及其中的所有卡片。你确定你要继续吗?" + "deleteColumnConfirmation": "这将删除该组及其中的所有卡片。你确定你要继续吗?", + "groupActions": "组操作" }, "hiddenGroupSection": { "sectionTitle": "私密组", diff --git a/frontend/resources/translations/zh-TW.json b/frontend/resources/translations/zh-TW.json index fe62360354..563aeb478d 100644 --- a/frontend/resources/translations/zh-TW.json +++ b/frontend/resources/translations/zh-TW.json @@ -894,10 +894,10 @@ "addToColumnBottomTooltip": "在底端新增卡片", "renameColumn": "重新命名", "hideColumn": "隱藏", - "groupActions": "群組操作", "newGroup": "新增群組", "deleteColumn": "刪除", - "deleteColumnConfirmation": "這將刪除此群組及其中所有卡片。\n您確定要繼續嗎?" + "deleteColumnConfirmation": "這將刪除此群組及其中所有卡片。\n您確定要繼續嗎?", + "groupActions": "群組操作" }, "hiddenGroupSection": { "sectionTitle": "隱藏的群組", From 8732c3c28b7414d95c97a519c841ffd9d1a17f59 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Sun, 3 Mar 2024 08:36:12 +0700 Subject: [PATCH 6/8] feat: members settings (#4788) * feat: add member settings * feat: fetch workspace members from server * feat: add translations * feat: implement invite feature * feat: support inviting people via email * feat: support updating member role * feat: add feature flag to control the visibilty of members settings --- .../header/document_header_node_widget.dart | 2 + .../lib/shared/feature_flags.dart | 25 + .../settings/settings_dialog_bloc.dart | 1 + .../settings/settings_dialog.dart | 14 +- .../members/workspace_member_bloc.dart | 158 ++++++ .../members/workspace_member_page.dart | 478 ++++++++++++++++++ .../settings/widgets/settings_menu.dart | 123 ++--- .../appflowy_result/lib/src/result.dart | 24 + .../lib/widget/buttons/primary_button.dart | 18 +- .../lib/widget/rounded_button.dart | 3 + .../flowy_icons/24x/invite_member_link.svg | 10 + frontend/resources/translations/en.json | 19 +- frontend/rust-lib/flowy-user/src/event_map.rs | 8 +- 13 files changed, 808 insertions(+), 75 deletions(-) create mode 100644 frontend/appflowy_flutter/lib/shared/feature_flags.dart create mode 100644 frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/members/workspace_member_bloc.dart create mode 100644 frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/members/workspace_member_page.dart create mode 100644 frontend/resources/flowy_icons/24x/invite_member_link.svg diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/document_header_node_widget.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/document_header_node_widget.dart index 78e458f7ab..9ca7f9ec8c 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/document_header_node_widget.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/document_header_node_widget.dart @@ -7,6 +7,7 @@ import 'package:appflowy/plugins/base/emoji/emoji_picker_screen.dart'; import 'package:appflowy/plugins/base/icon/icon_picker.dart'; import 'package:appflowy/plugins/document/application/doc_bloc.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/image/custom_image_block_component.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/image/image_util.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/image/upload_image_menu.dart'; import 'package:appflowy/plugins/document/presentation/editor_style.dart'; @@ -176,6 +177,7 @@ class _DocumentHeaderNodeWidgetState extends State { DocumentHeaderBlockKeys.coverDetails: coverDetails, DocumentHeaderBlockKeys.icon: widget.node.attributes[DocumentHeaderBlockKeys.icon], + CustomImageBlockKeys.imageType: '1', }; if (cover != null) { attributes[DocumentHeaderBlockKeys.coverType] = cover.$1.toString(); diff --git a/frontend/appflowy_flutter/lib/shared/feature_flags.dart b/frontend/appflowy_flutter/lib/shared/feature_flags.dart new file mode 100644 index 0000000000..8e94c46d61 --- /dev/null +++ b/frontend/appflowy_flutter/lib/shared/feature_flags.dart @@ -0,0 +1,25 @@ +/// The [FeatureFlag] is used to control the front-end features of the app. +/// +/// For example, if your feature is still under development, +/// you can set the value to `false` to hide the feature. +enum FeatureFlag { + // Feature flags + + // used to control the visibility of the collaborative workspace feature + // if it's on, you can see the workspace list and the workspace settings + // in the top-left corner of the app + collaborativeWorkspace, + + // used to control the visibility of the members settings + // if it's on, you can see the members settings in the settings page + membersSettings; + + bool get isOn { + switch (this) { + case FeatureFlag.collaborativeWorkspace: + return false; + case FeatureFlag.membersSettings: + return false; + } + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/settings_dialog_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/settings_dialog_bloc.dart index f1f369725e..8f40d706f6 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/settings_dialog_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/settings_dialog_bloc.dart @@ -16,6 +16,7 @@ enum SettingsPage { notifications, cloud, shortcuts, + member, } class SettingsDialogBloc diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/settings_dialog.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/settings_dialog.dart index c57378ce71..c24a6a9fab 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/settings_dialog.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/settings_dialog.dart @@ -1,18 +1,20 @@ -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/startup/startup.dart'; +import 'package:appflowy/workspace/application/settings/settings_dialog_bloc.dart'; +import 'package:appflowy/workspace/presentation/settings/widgets/members/workspace_member_page.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/settings_appearance_view.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/settings_customize_shortcuts_view.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/settings_file_system_view.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/settings_language_view.dart'; -import 'package:appflowy/workspace/presentation/settings/widgets/settings_user_view.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/settings_menu.dart'; -import 'package:appflowy/workspace/application/settings/settings_dialog_bloc.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:appflowy/workspace/presentation/settings/widgets/settings_notifications_view.dart'; +import 'package:appflowy/workspace/presentation/settings/widgets/settings_user_view.dart'; import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; + import 'widgets/setting_cloud.dart'; const _dialogHorizontalPadding = EdgeInsets.symmetric(horizontal: 12); @@ -110,6 +112,8 @@ class SettingsDialog extends StatelessWidget { ); case SettingsPage.shortcuts: return const SettingsCustomizeShortcutsWrapper(); + case SettingsPage.member: + return WorkspaceMembersPage(userProfile: user); default: return const SizedBox.shrink(); } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/members/workspace_member_bloc.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/members/workspace_member_bloc.dart new file mode 100644 index 0000000000..8bc317390e --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/members/workspace_member_bloc.dart @@ -0,0 +1,158 @@ +import 'package:appflowy_backend/dispatch/dispatch.dart'; +import 'package:appflowy_backend/log.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; +import 'package:collection/collection.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'workspace_member_bloc.freezed.dart'; + +// 1. get the workspace members +// 2. display the content based on the user role +// Owner: +// - invite member button +// - delete member button +// - member list +// Member: +// Guest: +// - member list +class WorkspaceMemberBloc + extends Bloc { + WorkspaceMemberBloc({ + required this.userProfile, + }) : super(WorkspaceMemberState.initial()) { + on((event, emit) async { + await event.map( + getWorkspaceMembers: (_) async { + final members = await _getWorkspaceMembers(); + final myRole = _getMyRole(members); + emit( + state.copyWith( + members: members, + myRole: myRole, + ), + ); + }, + addWorkspaceMember: (e) async { + await _addWorkspaceMember(e.email); + add(const WorkspaceMemberEvent.getWorkspaceMembers()); + }, + removeWorkspaceMember: (e) async { + await _removeWorkspaceMember(e.email); + add(const WorkspaceMemberEvent.getWorkspaceMembers()); + }, + updateWorkspaceMember: (e) async { + await _updateWorkspaceMember(e.email, e.role); + add(const WorkspaceMemberEvent.getWorkspaceMembers()); + }, + ); + }); + } + + final UserProfilePB userProfile; + + Future> _getWorkspaceMembers() async { + // will the current workspace be synced across the app? + final currentWorkspace = await FolderEventReadCurrentWorkspace().send(); + return currentWorkspace.fold((s) async { + final data = QueryWorkspacePB()..workspaceId = s.id; + final result = await UserEventGetWorkspaceMember(data).send(); + return result.fold((s) => s.items, (e) { + Log.error('Failed to read workspace members: $e'); + return []; + }); + }, (e) { + Log.error('Failed to read current workspace: $e'); + return []; + }); + } + + AFRolePB _getMyRole(List members) { + final role = members + .firstWhereOrNull( + (e) => e.email == userProfile.email, + ) + ?.role; + if (role == null) { + Log.error('Failed to get my role'); + return AFRolePB.Guest; + } + return role; + } + + Future _addWorkspaceMember(String email) async { + final currentWorkspace = await FolderEventReadCurrentWorkspace().send(); + return currentWorkspace.fold((s) async { + final data = AddWorkspaceMemberPB() + ..workspaceId = s.id + ..email = email; + final result = await UserEventAddWorkspaceMember(data).send(); + result.fold((s) { + Log.info('Added workspace member: $data'); + }, (e) { + Log.error('Failed to add workspace member: $e'); + }); + }, (e) { + Log.error('Failed to read current workspace: $e'); + }); + } + + Future _removeWorkspaceMember(String email) async { + final currentWorkspace = await FolderEventReadCurrentWorkspace().send(); + return currentWorkspace.fold((s) async { + final data = RemoveWorkspaceMemberPB() + ..workspaceId = s.id + ..email = email; + final result = await UserEventRemoveWorkspaceMember(data).send(); + result.fold((s) { + Log.info('Removed workspace member: $data'); + }, (e) { + Log.error('Failed to remove workspace member: $e'); + }); + }, (e) { + Log.error('Failed to read current workspace: $e'); + }); + } + + Future _updateWorkspaceMember(String email, AFRolePB role) async { + final currentWorkspace = await FolderEventReadCurrentWorkspace().send(); + return currentWorkspace.fold((s) async { + final data = UpdateWorkspaceMemberPB() + ..workspaceId = s.id + ..email = email + ..role = role; + final result = await UserEventUpdateWorkspaceMember(data).send(); + result.fold((s) { + Log.info('Updated workspace member: $data'); + }, (e) { + Log.error('Failed to update workspace member: $e'); + }); + }, (e) { + Log.error('Failed to read current workspace: $e'); + }); + } +} + +@freezed +class WorkspaceMemberEvent with _$WorkspaceMemberEvent { + const factory WorkspaceMemberEvent.getWorkspaceMembers() = + GetWorkspaceMembers; + const factory WorkspaceMemberEvent.addWorkspaceMember(String email) = + AddWorkspaceMember; + const factory WorkspaceMemberEvent.removeWorkspaceMember(String email) = + RemoveWorkspaceMember; + const factory WorkspaceMemberEvent.updateWorkspaceMember( + String email, + AFRolePB role, + ) = UpdateWorkspaceMember; +} + +@freezed +class WorkspaceMemberState with _$WorkspaceMemberState { + const factory WorkspaceMemberState({ + @Default([]) List members, + @Default(AFRolePB.Guest) AFRolePB myRole, + }) = _WorkspaceMemberState; + + factory WorkspaceMemberState.initial() => const WorkspaceMemberState(); +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/members/workspace_member_page.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/members/workspace_member_page.dart new file mode 100644 index 0000000000..f43e376340 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/members/workspace_member_page.dart @@ -0,0 +1,478 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/workspace/presentation/home/toast.dart'; +import 'package:appflowy/workspace/presentation/settings/widgets/members/workspace_member_bloc.dart'; +import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flowy_infra_ui/widget/buttons/primary_button.dart'; +import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; +import 'package:flowy_infra_ui/widget/rounded_button.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:string_validator/string_validator.dart'; + +class WorkspaceMembersPage extends StatelessWidget { + const WorkspaceMembersPage({ + super.key, + required this.userProfile, + }); + + final UserProfilePB userProfile; + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => WorkspaceMemberBloc(userProfile: userProfile) + ..add( + const WorkspaceMemberEvent.getWorkspaceMembers(), + ), + child: BlocBuilder( + builder: (context, state) { + return SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // title + FlowyText.semibold( + LocaleKeys.settings_appearance_members_title.tr(), + fontSize: 20, + ), + if (state.myRole.canInvite) const _InviteMember(), + if (state.members.isNotEmpty) + _MemberList( + members: state.members, + userProfile: userProfile, + myRole: state.myRole, + ), + ], + ), + ); + }, + ), + ); + } +} + +class _InviteMember extends StatefulWidget { + const _InviteMember(); + + @override + State<_InviteMember> createState() => _InviteMemberState(); +} + +class _InviteMemberState extends State<_InviteMember> { + final _emailController = TextEditingController(); + + @override + void dispose() { + _emailController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const VSpace(12.0), + FlowyText.semibold( + LocaleKeys.settings_appearance_members_inviteMembers.tr(), + fontSize: 16.0, + ), + const VSpace(8.0), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Expanded( + child: ConstrainedBox( + constraints: const BoxConstraints.tightFor( + height: 48.0, + ), + child: FlowyTextField( + controller: _emailController, + onEditingComplete: _inviteMember, + ), + ), + ), + const HSpace(10.0), + SizedBox( + height: 48.0, + child: IntrinsicWidth( + child: RoundedTextButton( + title: LocaleKeys.settings_appearance_members_sendInvite.tr(), + padding: + const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + onPressed: _inviteMember, + ), + ), + ), + ], + ), + const VSpace(16.0), + PrimaryButton( + backgroundColor: const Color(0xFFE0E0E0), + child: Padding( + padding: const EdgeInsets.only( + left: 20, + right: 24, + top: 8, + bottom: 8, + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const FlowySvg( + FlowySvgs.invite_member_link_m, + color: Colors.black, + ), + const HSpace(8.0), + FlowyText( + LocaleKeys.settings_appearance_members_copyInviteLink.tr(), + color: Colors.black, + ), + ], + ), + ), + onPressed: () { + showSnackBarMessage(context, 'not implemented'); + }, + ), + const VSpace(16.0), + const Divider( + height: 1.0, + thickness: 1.0, + ), + ], + ); + } + + void _inviteMember() { + final email = _emailController.text; + if (!isEmail(email)) { + showSnackBarMessage( + context, + LocaleKeys.settings_appearance_members_emailInvalidError.tr(), + ); + return; + } + context + .read() + .add(WorkspaceMemberEvent.addWorkspaceMember(email)); + showSnackBarMessage( + context, + LocaleKeys.settings_appearance_members_emailSent.tr(), + ); + } +} + +class _MemberList extends StatelessWidget { + const _MemberList({ + required this.members, + required this.myRole, + required this.userProfile, + }); + + final List members; + final AFRolePB myRole; + final UserProfilePB userProfile; + + @override + Widget build(BuildContext context) { + return Column( + children: [ + const VSpace(16.0), + SeparatedColumn( + crossAxisAlignment: CrossAxisAlignment.start, + separatorBuilder: () => const Divider(), + children: [ + const _MemberListHeader(), + ...members.map( + (member) => _MemberItem( + member: member, + myRole: myRole, + userProfile: userProfile, + ), + ), + ], + ), + ], + ); + } +} + +class _MemberListHeader extends StatelessWidget { + const _MemberListHeader(); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + FlowyText.semibold( + LocaleKeys.settings_appearance_members_label.tr(), + fontSize: 16.0, + ), + const VSpace(16.0), + Row( + children: [ + Expanded( + child: FlowyText.semibold( + LocaleKeys.settings_appearance_members_user.tr(), + fontSize: 14.0, + ), + ), + Expanded( + child: FlowyText.semibold( + LocaleKeys.settings_appearance_members_role.tr(), + fontSize: 14.0, + ), + ), + const HSpace(28.0), + ], + ), + ], + ); + } +} + +class _MemberItem extends StatelessWidget { + const _MemberItem({ + required this.member, + required this.myRole, + required this.userProfile, + }); + + final WorkspaceMemberPB member; + final AFRolePB myRole; + final UserProfilePB userProfile; + + @override + Widget build(BuildContext context) { + final textColor = member.role.isOwner ? Theme.of(context).hintColor : null; + return Row( + children: [ + Expanded( + child: FlowyText.medium( + member.name, + color: textColor, + fontSize: 14.0, + ), + ), + Expanded( + child: member.role.isOwner || !myRole.canUpdate + ? FlowyText.medium( + member.role.description, + color: textColor, + fontSize: 14.0, + ) + : _MemberRoleActionList( + member: member, + ), + ), + myRole.canDelete && + member.email != userProfile.email // can't delete self + ? _MemberMoreActionList(member: member) + : const HSpace(28.0), + ], + ); + } +} + +enum _MemberMoreAction { + delete, +} + +class _MemberMoreActionList extends StatelessWidget { + const _MemberMoreActionList({ + required this.member, + }); + + final WorkspaceMemberPB member; + + @override + Widget build(BuildContext context) { + return PopoverActionList<_MemberMoreActionWrapper>( + asBarrier: true, + direction: PopoverDirection.bottomWithCenterAligned, + actions: _MemberMoreAction.values + .map((e) => _MemberMoreActionWrapper(e, member)) + .toList(), + buildChild: (controller) { + return FlowyButton( + useIntrinsicWidth: true, + text: const FlowySvg( + FlowySvgs.three_dots_vertical_s, + ), + onTap: () { + controller.show(); + }, + ); + }, + onSelected: (action, controller) async { + switch (action.inner) { + case _MemberMoreAction.delete: + context.read().add( + WorkspaceMemberEvent.removeWorkspaceMember( + action.member.email, + ), + ); + break; + } + controller.close(); + }, + ); + } +} + +class _MemberMoreActionWrapper extends ActionCell { + _MemberMoreActionWrapper(this.inner, this.member); + + final _MemberMoreAction inner; + final WorkspaceMemberPB member; + + @override + String get name { + switch (inner) { + case _MemberMoreAction.delete: + return LocaleKeys.settings_appearance_members_removeFromWorkspace.tr(); + } + } +} + +class _MemberRoleActionList extends StatelessWidget { + const _MemberRoleActionList({ + required this.member, + }); + + final WorkspaceMemberPB member; + + @override + Widget build(BuildContext context) { + return PopoverActionList<_MemberRoleActionWrapper>( + asBarrier: true, + direction: PopoverDirection.bottomWithLeftAligned, + actions: [AFRolePB.Member, AFRolePB.Guest] + .map((e) => _MemberRoleActionWrapper(e, member)) + .toList(), + offset: const Offset(0, 10), + buildChild: (controller) { + return MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () => controller.show(), + child: Row( + children: [ + FlowyText.medium( + member.role.description, + fontSize: 14.0, + ), + const HSpace(8.0), + const FlowySvg( + FlowySvgs.drop_menu_show_s, + ), + ], + ), + ), + ); + }, + onSelected: (action, controller) async { + switch (action.inner) { + case AFRolePB.Member: + case AFRolePB.Guest: + context.read().add( + WorkspaceMemberEvent.updateWorkspaceMember( + action.member.email, + action.inner, + ), + ); + break; + case AFRolePB.Owner: + break; + } + controller.close(); + }, + ); + } +} + +class _MemberRoleActionWrapper extends ActionCell { + _MemberRoleActionWrapper(this.inner, this.member); + + final AFRolePB inner; + final WorkspaceMemberPB member; + + @override + Widget? rightIcon(Color iconColor) { + return SizedBox( + width: 58.0, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + FlowyTooltip( + message: tooltip, + child: const FlowySvg( + FlowySvgs.information_s, + // color: iconColor, + ), + ), + const Spacer(), + if (member.role == inner) + const FlowySvg( + FlowySvgs.checkmark_tiny_s, + ), + ], + ), + ); + } + + @override + String get name { + switch (inner) { + case AFRolePB.Guest: + return LocaleKeys.settings_appearance_members_guest.tr(); + case AFRolePB.Member: + return LocaleKeys.settings_appearance_members_member.tr(); + case AFRolePB.Owner: + return LocaleKeys.settings_appearance_members_owner.tr(); + } + throw UnimplementedError('Unknown role: $inner'); + } + + String get tooltip { + switch (inner) { + case AFRolePB.Guest: + return LocaleKeys.settings_appearance_members_guestHintText.tr(); + case AFRolePB.Member: + return LocaleKeys.settings_appearance_members_memberHintText.tr(); + case AFRolePB.Owner: + return ''; + } + throw UnimplementedError('Unknown role: $inner'); + } +} + +extension on AFRolePB { + bool get isOwner => this == AFRolePB.Owner; + + bool get canInvite => isOwner; + + bool get canDelete => isOwner; + + bool get canUpdate => isOwner; + + String get description { + switch (this) { + case AFRolePB.Owner: + return LocaleKeys.settings_appearance_members_owner.tr(); + case AFRolePB.Member: + return LocaleKeys.settings_appearance_members_member.tr(); + case AFRolePB.Guest: + return LocaleKeys.settings_appearance_members_guest.tr(); + } + throw UnimplementedError('Unknown role: $this'); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_menu.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_menu.dart index a5f3e889cc..278194bb4d 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_menu.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_menu.dart @@ -1,7 +1,9 @@ import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/shared/feature_flags.dart'; import 'package:appflowy/workspace/application/settings/settings_dialog_bloc.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/settings_menu_element.dart'; import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; class SettingsMenu extends StatelessWidget { @@ -16,64 +18,69 @@ class SettingsMenu extends StatelessWidget { @override Widget build(BuildContext context) { - return Column( - children: [ - SettingsMenuElement( - page: SettingsPage.appearance, - selectedPage: currentPage, - label: LocaleKeys.settings_menu_appearance.tr(), - icon: Icons.brightness_4, - changeSelectedPage: changeSelectedPage, - ), - const SizedBox(height: 10), - SettingsMenuElement( - page: SettingsPage.language, - selectedPage: currentPage, - label: LocaleKeys.settings_menu_language.tr(), - icon: Icons.translate, - changeSelectedPage: changeSelectedPage, - ), - const SizedBox(height: 10), - SettingsMenuElement( - page: SettingsPage.files, - selectedPage: currentPage, - label: LocaleKeys.settings_menu_files.tr(), - icon: Icons.file_present_outlined, - changeSelectedPage: changeSelectedPage, - ), - const SizedBox(height: 10), - SettingsMenuElement( - page: SettingsPage.user, - selectedPage: currentPage, - label: LocaleKeys.settings_menu_user.tr(), - icon: Icons.account_box_outlined, - changeSelectedPage: changeSelectedPage, - ), - const SizedBox(height: 10), - SettingsMenuElement( - page: SettingsPage.notifications, - selectedPage: currentPage, - label: LocaleKeys.settings_menu_notifications.tr(), - icon: Icons.notifications_outlined, - changeSelectedPage: changeSelectedPage, - ), - const SizedBox(height: 10), - SettingsMenuElement( - page: SettingsPage.cloud, - selectedPage: currentPage, - label: LocaleKeys.settings_menu_cloudSettings.tr(), - icon: Icons.sync, - changeSelectedPage: changeSelectedPage, - ), - const SizedBox(height: 10), - SettingsMenuElement( - page: SettingsPage.shortcuts, - selectedPage: currentPage, - label: LocaleKeys.settings_shortcuts_shortcutsLabel.tr(), - icon: Icons.cut, - changeSelectedPage: changeSelectedPage, - ), - ], + return SingleChildScrollView( + child: SeparatedColumn( + separatorBuilder: () => const SizedBox(height: 10), + children: [ + SettingsMenuElement( + page: SettingsPage.appearance, + selectedPage: currentPage, + label: LocaleKeys.settings_menu_appearance.tr(), + icon: Icons.brightness_4, + changeSelectedPage: changeSelectedPage, + ), + SettingsMenuElement( + page: SettingsPage.language, + selectedPage: currentPage, + label: LocaleKeys.settings_menu_language.tr(), + icon: Icons.translate, + changeSelectedPage: changeSelectedPage, + ), + SettingsMenuElement( + page: SettingsPage.files, + selectedPage: currentPage, + label: LocaleKeys.settings_menu_files.tr(), + icon: Icons.file_present_outlined, + changeSelectedPage: changeSelectedPage, + ), + SettingsMenuElement( + page: SettingsPage.user, + selectedPage: currentPage, + label: LocaleKeys.settings_menu_user.tr(), + icon: Icons.account_box_outlined, + changeSelectedPage: changeSelectedPage, + ), + SettingsMenuElement( + page: SettingsPage.notifications, + selectedPage: currentPage, + label: LocaleKeys.settings_menu_notifications.tr(), + icon: Icons.notifications_outlined, + changeSelectedPage: changeSelectedPage, + ), + SettingsMenuElement( + page: SettingsPage.cloud, + selectedPage: currentPage, + label: LocaleKeys.settings_menu_cloudSettings.tr(), + icon: Icons.sync, + changeSelectedPage: changeSelectedPage, + ), + SettingsMenuElement( + page: SettingsPage.shortcuts, + selectedPage: currentPage, + label: LocaleKeys.settings_shortcuts_shortcutsLabel.tr(), + icon: Icons.cut, + changeSelectedPage: changeSelectedPage, + ), + if (FeatureFlag.membersSettings.isOn) + SettingsMenuElement( + page: SettingsPage.member, + selectedPage: currentPage, + label: LocaleKeys.settings_appearance_members_label.tr(), + icon: Icons.people, + changeSelectedPage: changeSelectedPage, + ), + ], + ), ); } } diff --git a/frontend/appflowy_flutter/packages/appflowy_result/lib/src/result.dart b/frontend/appflowy_flutter/packages/appflowy_result/lib/src/result.dart index ac056f3b97..88d3051332 100644 --- a/frontend/appflowy_flutter/packages/appflowy_result/lib/src/result.dart +++ b/frontend/appflowy_flutter/packages/appflowy_result/lib/src/result.dart @@ -14,6 +14,14 @@ abstract class FlowyResult { bool isFailure(); S? toNullable(); + + void onSuccess( + void Function(S s) onSuccess, + ); + + void onFailure( + void Function(F f) onFailure, + ); } class FlowySuccess implements FlowyResult { @@ -64,6 +72,14 @@ class FlowySuccess implements FlowyResult { S? toNullable() { return _value; } + + @override + void onSuccess(void Function(S success) onSuccess) { + onSuccess(_value); + } + + @override + void onFailure(void Function(F failure) onFailure) {} } class FlowyFailure implements FlowyResult { @@ -114,4 +130,12 @@ class FlowyFailure implements FlowyResult { S? toNullable() { return null; } + + @override + void onSuccess(void Function(S success) onSuccess) {} + + @override + void onFailure(void Function(F failure) onFailure) { + onFailure(_error); + } } diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/buttons/primary_button.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/buttons/primary_button.dart index a2fe2f091e..6207419009 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/buttons/primary_button.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/buttons/primary_button.dart @@ -1,5 +1,6 @@ import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flutter/material.dart'; + import 'base_styled_button.dart'; import 'secondary_button.dart'; @@ -25,15 +26,18 @@ class PrimaryTextButton extends StatelessWidget { } class PrimaryButton extends StatelessWidget { + const PrimaryButton({ + super.key, + required this.child, + this.onPressed, + this.mode = TextButtonMode.big, + this.backgroundColor, + }); + final Widget child; final VoidCallback? onPressed; final TextButtonMode mode; - - const PrimaryButton( - {super.key, - required this.child, - this.onPressed, - this.mode = TextButtonMode.big}); + final Color? backgroundColor; @override Widget build(BuildContext context) { @@ -41,7 +45,7 @@ class PrimaryButton extends StatelessWidget { minWidth: mode.size.width, minHeight: mode.size.height, contentPadding: EdgeInsets.zero, - bgColor: Theme.of(context).colorScheme.primary, + bgColor: backgroundColor ?? Theme.of(context).colorScheme.primary, hoverColor: Theme.of(context).colorScheme.primaryContainer, borderRadius: mode.borderRadius, onPressed: onPressed, diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/rounded_button.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/rounded_button.dart index d4e9ed48c1..11b71b7d28 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/rounded_button.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/rounded_button.dart @@ -13,6 +13,7 @@ class RoundedTextButton extends StatelessWidget { final Color? hoverColor; final Color? textColor; final double? fontSize; + final EdgeInsets padding; const RoundedTextButton({ super.key, @@ -26,6 +27,7 @@ class RoundedTextButton extends StatelessWidget { this.hoverColor, this.textColor, this.fontSize, + this.padding = const EdgeInsets.symmetric(horizontal: 8, vertical: 6), }); @override @@ -48,6 +50,7 @@ class RoundedTextButton extends StatelessWidget { fillColor: fillColor ?? Theme.of(context).colorScheme.primary, hoverColor: hoverColor ?? Theme.of(context).colorScheme.primaryContainer, + padding: padding, ), ), ); diff --git a/frontend/resources/flowy_icons/24x/invite_member_link.svg b/frontend/resources/flowy_icons/24x/invite_member_link.svg new file mode 100644 index 0000000000..c9cf445a8f --- /dev/null +++ b/frontend/resources/flowy_icons/24x/invite_member_link.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index 5ceecd7cc9..d105849aa6 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -397,7 +397,24 @@ "twentyFourHour": "Twenty four hour" }, "showNamingDialogWhenCreatingPage": "Show naming dialog when creating a page", - "enableRTLToolbarItems": "Enable RTL toolbar items" + "enableRTLToolbarItems": "Enable RTL toolbar items", + "members": { + "title": "Members Settings", + "inviteMembers": "Invite Members", + "sendInvite": "Send Invite", + "copyInviteLink": "Copy Invite Link", + "label": "Members", + "user": "User", + "role": "Role", + "removeFromWorkspace": "Remove from Workspace", + "owner": "Owner", + "guest": "Guest", + "member": "Member", + "memberHintText": "A member can read, comment, and edit pages. Invite members and guests.", + "guestHintText": "A Guest can read, react, comment, and can edit certain pages with permission.", + "emailInvalidError": "Invalid email, please check and try again", + "emailSent": "Email sent, please check the inbox" + } }, "files": { "copy": "Copy", diff --git a/frontend/rust-lib/flowy-user/src/event_map.rs b/frontend/rust-lib/flowy-user/src/event_map.rs index 6dbeb877b2..2bcd60bbf7 100644 --- a/frontend/rust-lib/flowy-user/src/event_map.rs +++ b/frontend/rust-lib/flowy-user/src/event_map.rs @@ -180,16 +180,16 @@ pub enum UserEvent { #[event(output = "NotificationSettingsPB")] GetNotificationSettings = 36, - #[event(output = "AddWorkspaceMemberPB")] + #[event(input = "AddWorkspaceMemberPB")] AddWorkspaceMember = 37, - #[event(output = "RemoveWorkspaceMemberPB")] + #[event(input = "RemoveWorkspaceMemberPB")] RemoveWorkspaceMember = 38, - #[event(output = "UpdateWorkspaceMemberPB")] + #[event(input = "UpdateWorkspaceMemberPB")] UpdateWorkspaceMember = 39, - #[event(output = "QueryWorkspacePB")] + #[event(input = "QueryWorkspacePB", output = "RepeatedWorkspaceMemberPB")] GetWorkspaceMember = 40, #[event(input = "ImportAppFlowyDataPB")] From 1fadb153511f56a074592c050e4c8652803c8000 Mon Sep 17 00:00:00 2001 From: Daniil Date: Sun, 3 Mar 2024 04:41:33 +0300 Subject: [PATCH 7/8] chore: update russian translations (#4796) --- frontend/resources/translations/ru-RU.json | 171 +++++++++++++++++---- 1 file changed, 142 insertions(+), 29 deletions(-) diff --git a/frontend/resources/translations/ru-RU.json b/frontend/resources/translations/ru-RU.json index b7dae6282e..9a18b75272 100644 --- a/frontend/resources/translations/ru-RU.json +++ b/frontend/resources/translations/ru-RU.json @@ -2,8 +2,9 @@ "appName": "AppFlowy", "defaultUsername": "Я", "welcomeText": "Добро пожаловать в @:appName", + "welcomeTo": "Добро пожаловать в", "githubStarText": "Поставить звезду на GitHub", - "subscribeNewsletterText": "Подписаться на новостную рассылку", + "subscribeNewsletterText": "Подписаться на новости", "letsGoButtonText": "Быстрый старт", "title": "Заголовок", "youCanAlso": "Вы также можете", @@ -35,6 +36,7 @@ "loginStartWithAnonymous": "Начать анонимную сессию", "continueAnonymousUser": "Продолжить анонимную сессию", "buttonText": "Авторизация", + "signingInText": "Вход…", "forgotPassword": "Забыли пароль?", "emailHint": "Электронная почта", "passwordHint": "Пароль", @@ -59,7 +61,9 @@ "failedToLoad": "Что-то пошло не так! Не удалось загрузить рабочее пространство. Попробуйте закрыть все открытые экземпляры AppFlowy и повторите попытку.", "errorActions": { "reportIssue": "Сообщить о проблеме", - "reachOut": "Обратиться в Discord" + "reportIssueOnGithub": "Сообщить о проблеме на Github", + "exportLogFiles": "Экспорт логов", + "reachOut": "Написать в Discord" } }, "shareAction": { @@ -75,7 +79,12 @@ "large": "большой", "fontSize": "Размер шрифта", "import": "Импорт", - "moreOptions": "Дополнительные опции" + "moreOptions": "Дополнительные опции", + "wordCount": "Количество слов: {}", + "charCount": "Количество символов: {}", + "createdAt": "Создан в: {}", + "deleteView": "Удалить", + "duplicateView": "Дублировать" }, "importPanel": { "textAndMarkdown": "Текст и Markdown", @@ -232,7 +241,14 @@ "rename": "Переименовать", "helpCenter": "Центр помощи", "add": "Добавить", - "yes": "Да" + "yes": "Да", + "Done": "Готово", + "Cancel": "Отмена", + "clear": "Очистить", + "remove": "Удалить", + "dontRemove": "Не удалять", + "copyLink": "Скопировать ссылку", + "align": "Выровнять" }, "label": { "welcome": "Добро пожаловать!", @@ -277,9 +293,12 @@ "cloudLocal": "Локально", "cloudSupabase": "Supabase", "cloudSupabaseUrl": "Supabase URL", + "cloudSupabaseUrlCanNotBeEmpty": "URL-адрес Supabase не может быть пустым.", "cloudSupabaseAnonKey": "Анонимный ключ Supabase", "cloudSupabaseAnonKeyCanNotBeEmpty": "Анонимный ключ не может быть пустым, если URL Supabase не пуст", "cloudAppFlowy": "AppFlowy Cloud", + "cloudAppFlowySelfHost": "AppFlowy Cloud на своём сервере", + "appFlowyCloudUrlCanNotBeEmpty": "URL облака не может быть пустым.", "clickToCopy": "Нажмите, чтобы скопировать", "selfHostStart": "Если у вас нет сервера, пожалуйста, обратитесь к", "selfHostContent": "документации", @@ -289,6 +308,7 @@ "cloudWSURLHint": "Введите адрес вебсокета вашего сервера", "restartApp": "Перезапуск", "restartAppTip": "Перезапустите приложение, чтобы изменения вступили в силу. Обратите внимание, что это может привести к выходу из текущего аккаунта", + "changeServerTip": "После смены сервера необходимо нажать кнопку перезагрузки, чтобы изменения вступили в силу.", "enableEncryptPrompt": "Активируйте шифрование для защиты ваших данных с этим секретом. Храните его безопасно; после включения, он не может быть отключён. В случае потери секрета ваши данные будут также потеряны. Нажмите, чтобы скопировать", "inputEncryptPrompt": "Пожалуйста, введите ваш секрет шифрования для", "clickToCopySecret": "Нажмите, чтобы скопировать секрет", @@ -300,6 +320,7 @@ "openHistoricalUser": "Нажмите, чтобы открыть анонимный аккаунт", "customPathPrompt": "Хранение папки данных AppFlowy в папке с облачной синхронизацией, например на Google Диске, может представлять риск. Если база данных в этой папке будет доступна или изменена с нескольких мест одновременно, это может привести к конфликтам синхронизации и потенциальному повреждению данных", "importAppFlowyData": "Импортировать данные из внешней папки AppFlowy", + "importingAppFlowyDataTip": "Выполняется импорт данных. Пожалуйста, не закрывайте приложение", "importAppFlowyDataDescription": "Скопируйте данные из внешней папки данных AppFlowy и импортируйте их в текущую папку данных AppFlowy.", "importSuccess": "Папка данных AppFlowy успешно импортирована", "importFailed": "Не удалось импортировать папку данных AppFlowy", @@ -324,6 +345,7 @@ "dark": "Тёмная", "system": "Системная" }, + "fontScaleFactor": "Масштаб шрифта", "documentSettings": { "cursorColor": "Цвет курсора в документе", "selectionColor": "Цвет выделения в документе", @@ -377,11 +399,12 @@ "twelveHour": "12Ч", "twentyFourHour": "24Ч" }, - "showNamingDialogWhenCreatingPage": "Показывать диалоговое окно именования при создании страницы" + "showNamingDialogWhenCreatingPage": "Показывать диалоговое окно именования при создании страницы", + "enableRTLToolbarItems": "Включить режим панели слева-направо" }, "files": { "copy": "Копировать", - "defaultLocation": "Где сейчас хранятся ваши данные", + "defaultLocation": "Путь до хранилища", "exportData": "Экспорт данных", "doubleTapToCopy": "Нажмите дважды, чтобы скопировать путь", "restoreLocation": "Восстановить путь AppFlowy по умолчанию", @@ -445,10 +468,12 @@ "joinDiscord": "Присоединяйтесь к нам в Discord", "privacyPolicy": "Политика Конфиденциальности", "userAgreement": "Пользовательское Соглашение", + "termsAndConditions": "Условия и положения", "userprofileError": "Не удалось загрузить профиль пользователя", "userprofileErrorDescription": "Пожалуйста, попробуйте разлогиниться и войти снова, чтобы проверить, сохранится ли проблема.", "selectLayout": "Выбрать раскладку", - "selectStartingDay": "Выбрать день начала" + "selectStartingDay": "Выбрать день начала", + "version": "Версия" } }, "grid": { @@ -470,14 +495,14 @@ "typeAValue": "Введите значение...", "layout": "Вид", "databaseLayout": "Вид базы данных", + "viewList": "Представление базы данных", "editView": "Редактировать представление", "boardSettings": "Настройки доски", "calendarSettings": "Настройки календаря", "createView": "Новое представление", "duplicateView": "Дублировать представление", "deleteView": "Удалить представление", - "numberOfVisibleFields": "{} показано", - "viewList": "Представление базы данных" + "numberOfVisibleFields": "{} показано" }, "textFilter": { "contains": "Содержит", @@ -527,7 +552,25 @@ "onOrAfter": "Равно или после", "between": "Между", "empty": "Пусто", - "notEmpty": "Не пусто" + "notEmpty": "Не пусто", + "choicechipPrefix": { + "before": "До", + "after": "После", + "onOrBefore": "Не позднее", + "onOrAfter": "После", + "isEmpty": "Пусто", + "isNotEmpty": "Не пусто" + } + }, + "numberFilter": { + "equal": "Равно", + "notEqual": "Не равно", + "lessThan": "Меньше чем", + "greaterThan": "Больше, чем", + "lessThanOrEqualTo": "Меньше или равно", + "greaterThanOrEqualTo": "Больше или равно", + "isEmpty": "Пусто", + "isNotEmpty": "Не пусто" }, "field": { "hide": "Скрыть", @@ -546,6 +589,7 @@ "multiSelectFieldName": "Выбор нескольких", "urlFieldName": "URL", "checklistFieldName": "To-Do лист", + "relationFieldName": "Связь", "numberFormat": "Формат числа", "dateFormat": "Формат даты", "includeTime": "Время", @@ -576,7 +620,9 @@ "newProperty": "Новое свойство", "deleteFieldPromptMessage": "Вы уверены, что хотите удалить?", "newColumn": "Новый столбец", - "format": "Формат" + "format": "Формат", + "reminderOnDateTooltip": "В этой ячейке есть запланированное напоминание", + "optionAlreadyExist": "Вариант уже существует" }, "rowPage": { "newField": "Добавить новое поле", @@ -595,6 +641,9 @@ "sort": { "ascending": "По возрастанию", "descending": "По убыванию", + "by": "По", + "empty": "Нет активных сортировок", + "cannotFindCreatableField": "Не могу найти подходящее поле для сортировки", "deleteAllSorts": "Удалить все сортировки", "addSort": "Добавить сортировку" }, @@ -641,8 +690,24 @@ "hideComplete": "Скрыть выполненные задачи", "showComplete": "Показать все задачи" }, + "relation": { + "relatedDatabasePlaceLabel": "Связанная база данных", + "relatedDatabasePlaceholder": "Пусто", + "inRelatedDatabase": "В", + "emptySearchResult": "записей не найдено" + }, "menuName": "Сетка", - "referencedGridPrefix": "Просмотр" + "referencedGridPrefix": "Просмотр", + "calculate": "Рассчитать", + "calculationTypeLabel": { + "none": "Пусто", + "average": "Среднее", + "max": "Максимум", + "median": "Медиана", + "min": "Минимум", + "sum": "Сумма" + }, + "removeSorting": "Убрать сортировку?" }, "document": { "menuName": "Документ", @@ -745,17 +810,21 @@ "left": "Слева", "center": "По центру", "right": "Справа", - "defaultColor": "Цвет по умолчанию" + "defaultColor": "Цвет по умолчанию", + "depth": "Глубина" }, "image": { "copiedToPasteBoard": "Ссылка на изображение скопирована в буфер обмена", - "addAnImage": "Добавить изображение" + "addAnImage": "Добавить изображение", + "imageUploadFailed": "Не удалось загрузить изображение." }, "urlPreview": { - "copiedToPasteBoard": "Ссылка скопирована в буфер обмена" + "copiedToPasteBoard": "Ссылка скопирована в буфер обмена", + "convertToLink": "Сделать встроенной ссылкой" }, "outline": { - "addHeadingToCreateOutline": "Добавьте заголовки, чтобы создать оглавление." + "addHeadingToCreateOutline": "Добавьте заголовки, чтобы создать оглавление.", + "noMatchHeadings": "Соответствующие заголовки не найдены." }, "table": { "addAfter": "Добавить после", @@ -778,7 +847,12 @@ "toContinue": "чтобы продолжить", "newDatabase": "Новая база данных", "linkToDatabase": "Связать базу данных" - } + }, + "date": "Дата", + "emoji": "Эмодзи" + }, + "outlineBlock": { + "placeholder": "Оглавление" }, "textBlock": { "placeholder": "Введите '/' для команд" @@ -824,7 +898,10 @@ "saveImageToGallery": "Сохранить изображение", "failedToAddImageToGallery": "Ошибка добавления изображения в галерею", "successToAddImageToGallery": "Изображение успешно добавлено", - "unableToLoadImage": "Ошибка загрузки изображения" + "unableToLoadImage": "Ошибка загрузки изображения", + "maximumImageSize": "Максимальный поддерживаемый размер загружаемого изображения — 10 МБ.", + "uploadImageErrorImageSizeTooBig": "Размер изображения должен быть меньше 10 МБ.", + "imageIsUploading": "Изображение загружается" }, "codeBlock": { "language": { @@ -851,7 +928,9 @@ "page": { "label": "Ссылка на страницу", "tooltip": "Нажмите, чтобы открыть страницу" - } + }, + "deleted": "Удалено", + "deletedContent": "Этот контент не существует или был удален" }, "toolbar": { "resetToDefaultFont": "Восстановить по умолчанию" @@ -912,6 +991,10 @@ "previousMonth": "Предыдущий месяц", "nextMonth": "Следующий месяц" }, + "mobileEventScreen": { + "emptyTitle": "Мероприятий пока нет", + "emptyBody": "Нажмите кнопку «плюс», чтобы создать событие в этот день." + }, "settings": { "showWeekNumbers": "Показывать номера недель", "showWeekends": "Показывать выходные", @@ -1000,6 +1083,10 @@ "inlineActions": { "noResults": "Нет результатов", "pageReference": "Ссылка на страницу", + "docReference": "Ссылка на документ", + "boardReference": "Ссылка на доску", + "calReference": "Ссылка на календарь", + "gridReference": "Ссылка на таблицу", "date": "Дата", "reminder": { "groupTitle": "Напоминание", @@ -1008,11 +1095,28 @@ }, "datePicker": { "dateTimeFormatTooltip": "Измените формат даты и времени в настройках", - "dateFormat": "Date format", - "includeTime": "Include time", - "isRange": "End date", - "timeFormat": "Time format", - "clearDate": "Clear date" + "dateFormat": "Формат даты", + "includeTime": "Добавить время", + "isRange": "Дата завершения", + "timeFormat": "Формат времени", + "clearDate": "Очистить дату", + "reminderLabel": "Напоминание", + "selectReminder": "Выбрать напоминание", + "reminderOptions": { + "none": "Пусто", + "atTimeOfEvent": "Время события", + "fiveMinsBefore": "за 5 минут до", + "tenMinsBefore": "за 10 минут до этого", + "fifteenMinsBefore": "за 15 минут до этого", + "thirtyMinsBefore": "за 30 минут до", + "oneHourBefore": "за 1 час до", + "twoHoursBefore": "за 2 часа до", + "onDayOfEvent": "В день мероприятия", + "oneDayBefore": "за 1 день до", + "twoDaysBefore": "за 2 дня до", + "oneWeekBefore": "за 1 неделю до", + "custom": "Настраиваемое" + } }, "relativeDates": { "yesterday": "Вчера", @@ -1023,7 +1127,7 @@ "notificationHub": { "title": "Уведомления", "mobile": { - "title": "Updates" + "title": "Обновления" }, "emptyTitle": "Всё прочитано!", "emptyBody": "Никаких ожидающих уведомлений или действий. Наслаждайтесь спокойствием.", @@ -1059,7 +1163,8 @@ "replace": "Заменить", "replaceAll": "Заменить всё", "noResult": "Нет результатов", - "caseSensitive": "С учётом регистра" + "caseSensitive": "С учётом регистра", + "searchMore": "Выполните поиск, чтобы найти больше результатов" }, "error": { "weAreSorry": "Мы сожалеем", @@ -1068,6 +1173,7 @@ "editor": { "bold": "Жирный", "bulletedList": "Маркированный список", + "bulletedListShortForm": "Маркированный", "checkbox": "Чекбокс", "embedCode": "Встроенный код", "heading1": "H1", @@ -1076,9 +1182,11 @@ "highlight": "Выделить", "color": "Цвет", "image": "Изображение", + "date": "Дата", "italic": "Курсив", "link": "Ссылка", "numberedList": "Нумерованный список", + "numberedListShortForm": "Пронумерованный", "quote": "Цитировать", "strikethrough": "Зачёркнутый", "text": "Текст", @@ -1177,10 +1285,14 @@ "colClear": "Очистить контент", "rowClear": "Очистить содержимое строки", "slashPlaceHolder": "Введите '/' чтобы вставить блок, или начните писать.", - "typeSomething": "Введите что-либо..." + "typeSomething": "Введите что-либо...", + "toggleListShortForm": "Спойлер", + "quoteListShortForm": "Цитата", + "mathEquationShortForm": "Формула", + "codeBlockShortForm": "Код" }, "favorite": { - "noFavorite": "Нет избранной страницы", + "noFavorite": "Нет избранных страниц", "noFavoriteHintText": "Проведите по странице влево, чтобы добавить ее в избранное." }, "cardDetails": { @@ -1201,5 +1313,6 @@ "date": "Дата", "addField": "Добавить поле", "userIcon": "Пользовательская иконка" - } + }, + "noLogFiles": "Нет файлов журналов" } From 63464cbf2e66b5a64b8962e43b542db96d9fde43 Mon Sep 17 00:00:00 2001 From: Mathias Mogensen <42929161+Xazin@users.noreply.github.com> Date: Sun, 3 Mar 2024 22:04:59 +0100 Subject: [PATCH 8/8] fix: launch url on all platforms (#4797) * fix: error handled launchUrl * chore: update links to use new subdomain * feat: show toast if onFailure is not provided --- .../lib/core/helpers/url_launcher.dart | 50 +++++++++++++++++++ .../setting/about/about_setting_group.dart | 9 ++-- .../setting/support_setting_group.dart | 9 ++-- .../widgets/flowy_mobile_state_container.dart | 11 ++-- .../database/application/cell/cell_cache.dart | 2 +- .../database/application/row/row_cache.dart | 6 ++- .../database/application/view/view_cache.dart | 5 +- .../desktop_grid/desktop_grid_url_cell.dart | 11 ++-- .../mobile_grid/mobile_grid_url_cell.dart | 9 ++-- .../mobile_row_detail_url_cell.dart | 9 ++-- .../link_preview/custom_link_preview.dart | 7 +-- .../openai/util/learn_more_action.dart | 12 ++--- .../document/presentation/editor_style.dart | 3 +- .../auth/af_cloud_auth_service.dart | 3 +- .../screens/skip_log_in_screen.dart | 22 +++----- .../home/errors/workspace_failed_screen.dart | 9 ++-- .../workspace/presentation/home/toast.dart | 3 +- .../widgets/setting_appflowy_cloud.dart | 20 +++----- ...etting_file_import_appflowy_data_view.dart | 20 +++----- .../widgets/setting_supabase_cloud.dart | 22 +++----- ...settings_file_customize_location_view.dart | 11 ++-- .../theme_upload_learn_more_button.dart | 49 +++++++++--------- .../widgets/float_bubble/question_bubble.dart | 30 ++++------- frontend/resources/translations/en.json | 1 + 24 files changed, 180 insertions(+), 153 deletions(-) create mode 100644 frontend/appflowy_flutter/lib/core/helpers/url_launcher.dart diff --git a/frontend/appflowy_flutter/lib/core/helpers/url_launcher.dart b/frontend/appflowy_flutter/lib/core/helpers/url_launcher.dart new file mode 100644 index 0000000000..28820b9968 --- /dev/null +++ b/frontend/appflowy_flutter/lib/core/helpers/url_launcher.dart @@ -0,0 +1,50 @@ +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; + +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/workspace/presentation/home/toast.dart'; +import 'package:appflowy_backend/log.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:url_launcher/url_launcher.dart' as launcher; + +typedef OnFailureCallback = void Function(Uri uri); + +Future afLaunchUrl( + Uri uri, { + BuildContext? context, + OnFailureCallback? onFailure, + launcher.LaunchMode mode = launcher.LaunchMode.platformDefault, + String? webOnlyWindowName, +}) async { + try { + return await launcher.launchUrl( + uri, + mode: mode, + webOnlyWindowName: webOnlyWindowName, + ); + } on PlatformException catch (e) { + Log.error("Failed to open uri: $e"); + if (onFailure != null) { + onFailure(uri); + } else { + showMessageToast( + LocaleKeys.failedToOpenUrl.tr(args: [e.message ?? "PlatformException"]), + context: context, + ); + } + } + + return false; +} + +Future afLaunchUrlString(String url) async { + try { + final uri = Uri.parse(url); + + await launcher.launchUrl(uri); + } on PlatformException catch (e) { + Log.error("Failed to open uri: $e"); + } on FormatException catch (e) { + Log.error("Failed to parse url: $e"); + } +} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/setting/about/about_setting_group.dart b/frontend/appflowy_flutter/lib/mobile/presentation/setting/about/about_setting_group.dart index dd4f2c2660..66d25c58c6 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/setting/about/about_setting_group.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/setting/about/about_setting_group.dart @@ -1,9 +1,10 @@ +import 'package:flutter/material.dart'; + +import 'package:appflowy/core/helpers/url_launcher.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/startup/tasks/device_info_task.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; import '../widgets/widgets.dart'; @@ -22,14 +23,14 @@ class AboutSettingGroup extends StatelessWidget { trailing: const Icon( Icons.chevron_right, ), - onTap: () => safeLaunchUrl('https://appflowy.io/privacy/app'), + onTap: () => afLaunchUrlString('https://appflowy.io/privacy/app'), ), MobileSettingItem( name: LocaleKeys.settings_mobile_termsAndConditions.tr(), trailing: const Icon( Icons.chevron_right, ), - onTap: () => safeLaunchUrl('https://appflowy.io/terms/app'), + onTap: () => afLaunchUrlString('https://appflowy.io/terms/app'), ), MobileSettingItem( name: LocaleKeys.settings_mobile_version.tr(), diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/setting/support_setting_group.dart b/frontend/appflowy_flutter/lib/mobile/presentation/setting/support_setting_group.dart index 03dc888c4c..89e306b18b 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/setting/support_setting_group.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/setting/support_setting_group.dart @@ -1,12 +1,13 @@ import 'dart:io'; +import 'package:flutter/material.dart'; + +import 'package:appflowy/core/helpers/url_launcher.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart'; import 'package:appflowy/mobile/presentation/widgets/widgets.dart'; import 'package:appflowy/util/share_log_files.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'widgets/widgets.dart'; @@ -28,7 +29,7 @@ class SupportSettingGroup extends StatelessWidget { trailing: const Icon( Icons.chevron_right, ), - onTap: () => safeLaunchUrl('https://discord.gg/JucBXeU2FE'), + onTap: () => afLaunchUrlString('https://discord.gg/JucBXeU2FE'), ), MobileSettingItem( name: LocaleKeys.workspace_errorActions_reportIssue.tr(), @@ -73,7 +74,7 @@ class _ReportIssuesWidget extends StatelessWidget { text: LocaleKeys.workspace_errorActions_reportIssueOnGithub.tr(), onTap: () { final String os = Platform.operatingSystem; - safeLaunchUrl( + afLaunchUrlString( 'https://github.com/AppFlowy-IO/AppFlowy/issues/new?assignees=&labels=&projects=&template=bug_report.yaml&title=[Bug]%20Mobile:%20&version=$version&os=$os', ); }, diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/widgets/flowy_mobile_state_container.dart b/frontend/appflowy_flutter/lib/mobile/presentation/widgets/flowy_mobile_state_container.dart index 9497d779dd..8aea36fbb9 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/widgets/flowy_mobile_state_container.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/widgets/flowy_mobile_state_container.dart @@ -1,9 +1,10 @@ import 'dart:io'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; + +import 'package:appflowy/core/helpers/url_launcher.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:easy_localization/easy_localization.dart'; import 'package:package_info_plus/package_info_plus.dart'; enum _FlowyMobileStateContainerType { @@ -80,7 +81,7 @@ class FlowyMobileStateContainer extends StatelessWidget { onPressed: () { final String? version = snapshot.data?.version; final String os = Platform.operatingSystem; - safeLaunchUrl( + afLaunchUrlString( 'https://github.com/AppFlowy-IO/AppFlowy/issues/new?assignees=&labels=&projects=&template=bug_report.yaml&title=[Bug]%20Mobile:%20&version=$version&os=$os&context=Error%20log:%20$errorMsg', ); }, @@ -90,7 +91,7 @@ class FlowyMobileStateContainer extends StatelessWidget { ), OutlinedButton( onPressed: () => - safeLaunchUrl('https://discord.gg/JucBXeU2FE'), + afLaunchUrlString('https://discord.gg/JucBXeU2FE'), child: Text( LocaleKeys.workspace_errorActions_reachOut.tr(), ), diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/cell/cell_cache.dart b/frontend/appflowy_flutter/lib/plugins/database/application/cell/cell_cache.dart index 0c4c5c0697..171f86f11d 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/application/cell/cell_cache.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/application/cell/cell_cache.dart @@ -4,7 +4,7 @@ import 'cell_controller.dart'; /// CellMemCache is used to cache cell data of each block. /// We use CellContext to index the cell in the cache. -/// Read https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/architecture/frontend/grid +/// Read https://docs.appflowy.io/docs/documentation/software-contributions/architecture/frontend/frontend/grid /// for more information class CellMemCache { CellMemCache(); diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/row/row_cache.dart b/frontend/appflowy_flutter/lib/plugins/database/application/row/row_cache.dart index 9482c4add0..331dd159da 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/application/row/row_cache.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/application/row/row_cache.dart @@ -1,13 +1,15 @@ import 'dart:collection'; +import 'package:flutter/foundation.dart'; + import 'package:appflowy/plugins/database/application/field/field_info.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; -import 'package:flutter/foundation.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import '../cell/cell_cache.dart'; import '../cell/cell_controller.dart'; + import 'row_list.dart'; import 'row_service.dart'; @@ -25,7 +27,7 @@ abstract mixin class RowLifeCycle { void onRowDisposed(); } -/// Read https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/architecture/frontend/grid for more information. +/// Read https://docs.appflowy.io/docs/documentation/software-contributions/architecture/frontend/frontend/grid for more information. class RowCache { RowCache({ diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/view/view_cache.dart b/frontend/appflowy_flutter/lib/plugins/database/application/view/view_cache.dart index 2c1a92783a..77670fb0bb 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/application/view/view_cache.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/application/view/view_cache.dart @@ -1,10 +1,13 @@ import 'dart:async'; import 'dart:collection'; + import 'package:appflowy/plugins/database/application/row/row_service.dart'; import 'package:appflowy_backend/log.dart'; + import '../defines.dart'; import '../field/field_controller.dart'; import '../row/row_cache.dart'; + import 'view_listener.dart'; class DatabaseViewCallbacks { @@ -30,7 +33,7 @@ class DatabaseViewCallbacks { final OnRowsDeleted? onRowsDeleted; } -/// Read https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/architecture/frontend/grid for more information +/// Read https://docs.appflowy.io/docs/documentation/software-contributions/architecture/frontend/frontend/grid for more information class DatabaseViewCache { DatabaseViewCache({ required this.viewId, diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_url_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_url_cell.dart index 2ecd035999..4f3e69bad1 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_url_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_url_cell.dart @@ -1,16 +1,17 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +import 'package:appflowy/core/helpers/url_launcher.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/database/application/cell/bloc/url_cell_bloc.dart'; import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart'; import 'package:appflowy/plugins/database/widgets/row/accessory/cell_accessory.dart'; import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; -import 'package:appflowy/plugins/database/application/cell/bloc/url_cell_bloc.dart'; import 'package:appflowy/workspace/presentation/home/toast.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:url_launcher/url_launcher_string.dart'; import '../editable_cell_skeleton/url.dart'; @@ -189,7 +190,7 @@ class _VisitURLAccessoryState extends State<_VisitURLAccessory> final shouldAddScheme = !['http', 'https'].any((pattern) => content.startsWith(pattern)); final url = shouldAddScheme ? 'http://$content' : content; - canLaunchUrlString(url).then((value) => launchUrlString(url)); + afLaunchUrlString(url); } } diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_url_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_url_cell.dart index 267ef65a37..6705a7d776 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_url_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_url_cell.dart @@ -1,13 +1,14 @@ +import 'package:flutter/material.dart'; + +import 'package:appflowy/core/helpers/url_launcher.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart'; +import 'package:appflowy/plugins/database/application/cell/bloc/url_cell_bloc.dart'; import 'package:appflowy/plugins/database/widgets/row/accessory/cell_accessory.dart'; import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; -import 'package:appflowy/plugins/database/application/cell/bloc/url_cell_bloc.dart'; import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; -import 'package:url_launcher/url_launcher_string.dart'; import '../editable_cell_skeleton/url.dart'; @@ -51,7 +52,7 @@ class MobileGridURLCellSkin extends IEditableURLCellSkin { final shouldAddScheme = !['http', 'https'] .any((pattern) => content.startsWith(pattern)); final url = shouldAddScheme ? 'http://$content' : content; - canLaunchUrlString(url).then((value) => launchUrlString(url)); + afLaunchUrlString(url); }, onLongPress: () => showMobileBottomSheet( context, diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_url_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_url_cell.dart index e48a82c054..ec4dc11826 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_url_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_url_cell.dart @@ -1,13 +1,14 @@ +import 'package:flutter/material.dart'; + +import 'package:appflowy/core/helpers/url_launcher.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart'; +import 'package:appflowy/plugins/database/application/cell/bloc/url_cell_bloc.dart'; import 'package:appflowy/plugins/database/widgets/row/accessory/cell_accessory.dart'; import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; -import 'package:appflowy/plugins/database/application/cell/bloc/url_cell_bloc.dart'; import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; -import 'package:url_launcher/url_launcher_string.dart'; import '../editable_cell_skeleton/url.dart'; @@ -34,7 +35,7 @@ class MobileRowDetailURLCellSkin extends IEditableURLCellSkin { final shouldAddScheme = !['http', 'https'] .any((pattern) => content.startsWith(pattern)); final url = shouldAddScheme ? 'http://$content' : content; - canLaunchUrlString(url).then((value) => launchUrlString(url)); + afLaunchUrlString(url); }, onLongPress: () => _showURLEditor(context, bloc, content), child: Container( diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/custom_link_preview.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/custom_link_preview.dart index cd7de6ff32..152e7ed20a 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/custom_link_preview.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/custom_link_preview.dart @@ -1,3 +1,6 @@ +import 'package:flutter/material.dart'; + +import 'package:appflowy/core/helpers/url_launcher.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/mobile/presentation/widgets/widgets.dart'; @@ -7,10 +10,8 @@ import 'package:appflowy/shared/appflowy_network_image.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:provider/provider.dart'; -import 'package:url_launcher/url_launcher_string.dart'; class CustomLinkPreviewWidget extends StatelessWidget { const CustomLinkPreviewWidget({ @@ -113,7 +114,7 @@ class CustomLinkPreviewWidget extends StatelessWidget { if (PlatformExtension.isDesktopOrWeb) { return InkWell( - onTap: () => launchUrlString(url), + onTap: () => afLaunchUrlString(url), child: child, ); } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/openai/util/learn_more_action.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/openai/util/learn_more_action.dart index 49b047c758..17e89b1bca 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/openai/util/learn_more_action.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/openai/util/learn_more_action.dart @@ -1,10 +1,8 @@ -import 'package:url_launcher/url_launcher.dart'; +import 'package:appflowy/core/helpers/url_launcher.dart'; + +const String learnMoreUrl = + 'https://docs.appflowy.io/docs/appflowy/product/appflowy-x-openai'; Future openLearnMorePage() async { - final uri = Uri.parse( - 'https://appflowy.gitbook.io/docs/essential-documentation/appflowy-x-openai', - ); - if (await canLaunchUrl(uri)) { - await launchUrl(uri); - } + await afLaunchUrlString(learnMoreUrl); } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_style.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_style.dart index 1bf65ecd84..9095f1841e 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_style.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_style.dart @@ -3,6 +3,7 @@ import 'dart:math'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; +import 'package:appflowy/core/helpers/url_launcher.dart'; import 'package:appflowy/plugins/document/application/document_appearance_cubit.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_block.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_item/utils.dart'; @@ -293,7 +294,7 @@ class EditorStyleCustomizer { ..onTap = () { final editorState = context.read(); if (editorState.selection == null) { - safeLaunchUrl(href); + afLaunchUrlString(href); return; } diff --git a/frontend/appflowy_flutter/lib/user/application/auth/af_cloud_auth_service.dart b/frontend/appflowy_flutter/lib/user/application/auth/af_cloud_auth_service.dart index 0140316ed2..aabbd6e286 100644 --- a/frontend/appflowy_flutter/lib/user/application/auth/af_cloud_auth_service.dart +++ b/frontend/appflowy_flutter/lib/user/application/auth/af_cloud_auth_service.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:appflowy/core/helpers/url_launcher.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/startup/tasks/appflowy_cloud_task.dart'; import 'package:appflowy/user/application/auth/auth_service.dart'; @@ -55,7 +56,7 @@ class AppFlowyCloudAuthService implements AuthService { (data) async { // Open the webview with oauth url final uri = Uri.parse(data.oauthUrl); - final isSuccess = await launchUrl( + final isSuccess = await afLaunchUrl( uri, mode: LaunchMode.externalApplication, webOnlyWindowName: '_self', diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/skip_log_in_screen.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/skip_log_in_screen.dart index 60f49937ec..b125abf93c 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/skip_log_in_screen.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/screens/skip_log_in_screen.dart @@ -1,4 +1,7 @@ +import 'package:flutter/material.dart'; + import 'package:appflowy/core/frameless_window.dart'; +import 'package:appflowy/core/helpers/url_launcher.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/startup/startup.dart'; @@ -15,9 +18,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/language.dart'; import 'package:flowy_infra/size.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:url_launcher/url_launcher.dart'; class SkipLogInScreen extends StatefulWidget { const SkipLogInScreen({super.key}); @@ -158,9 +159,8 @@ class SubscribeButtons extends StatelessWidget { fontColor: Theme.of(context).colorScheme.primary, hoverColor: Colors.transparent, fillColor: Colors.transparent, - onPressed: () => _launchURL( - 'https://github.com/AppFlowy-IO/appflowy', - ), + onPressed: () => + afLaunchUrlString('https://github.com/AppFlowy-IO/appflowy'), ), ], ), @@ -179,22 +179,14 @@ class SubscribeButtons extends StatelessWidget { fontColor: Theme.of(context).colorScheme.primary, hoverColor: Colors.transparent, fillColor: Colors.transparent, - onPressed: () => _launchURL('https://www.appflowy.io/blog'), + onPressed: () => + afLaunchUrlString('https://www.appflowy.io/blog'), ), ], ), ], ); } - - Future _launchURL(String url) async { - final uri = Uri.parse(url); - if (await canLaunchUrl(uri)) { - await launchUrl(uri); - } else { - throw 'Could not launch $url'; - } - } } class LanguageSelectorOnWelcomePage extends StatelessWidget { diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/errors/workspace_failed_screen.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/errors/workspace_failed_screen.dart index 60f3330408..68cac12dc5 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/errors/workspace_failed_screen.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/errors/workspace_failed_screen.dart @@ -1,11 +1,12 @@ import 'dart:io'; +import 'package:flutter/material.dart'; + +import 'package:appflowy/core/helpers/url_launcher.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/widget/rounded_button.dart'; import 'package:flowy_infra_ui/widget/spacing.dart'; -import 'package:flutter/material.dart'; import 'package:package_info_plus/package_info_plus.dart'; class WorkspaceFailedScreen extends StatefulWidget { @@ -51,7 +52,7 @@ class _WorkspaceFailedScreenState extends State { title: LocaleKeys.workspace_errorActions_reportIssue.tr(), height: 40, - onPressed: () => safeLaunchUrl( + onPressed: () => afLaunchUrlString( 'https://github.com/AppFlowy-IO/AppFlowy/issues/new?assignees=&labels=&projects=&template=bug_report.yaml&title=[Bug]%20Workspace%20failed%20to%20load&version=$version&os=$os', ), ), @@ -62,7 +63,7 @@ class _WorkspaceFailedScreenState extends State { title: LocaleKeys.workspace_errorActions_reachOut.tr(), height: 40, onPressed: () => - safeLaunchUrl('https://discord.gg/JucBXeU2FE'), + afLaunchUrlString('https://discord.gg/JucBXeU2FE'), ), ), ], diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/toast.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/toast.dart index 57c3590a25..c890aea90c 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/toast.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/toast.dart @@ -1,9 +1,10 @@ +import 'package:flutter/material.dart'; + import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/size.dart'; import 'package:flowy_infra_ui/style_widget/text.dart'; -import 'package:flutter/material.dart'; import 'package:fluttertoast/fluttertoast.dart'; class FlowyMessageToast extends StatelessWidget { diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart index 30c7da23f2..6805633f74 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart @@ -1,3 +1,7 @@ +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; + +import 'package:appflowy/core/helpers/url_launcher.dart'; import 'package:appflowy/env/cloud_env.dart'; import 'package:appflowy/env/env.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; @@ -6,7 +10,6 @@ import 'package:appflowy/workspace/application/settings/appflowy_cloud_urls_bloc import 'package:appflowy/workspace/presentation/settings/widgets/_restart_app_button.dart'; import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; import 'package:appflowy_backend/dispatch/dispatch.dart'; -import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-user/user_setting.pb.dart'; import 'package:appflowy_result/appflowy_result.dart'; @@ -14,10 +17,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/size.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/widget/error_page.dart'; -import 'package:flutter/gestures.dart'; -import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:url_launcher/url_launcher.dart'; class AppFlowyCloudViewSetting extends StatelessWidget { const AppFlowyCloudViewSetting({ @@ -227,7 +227,8 @@ class AppFlowySelfhostTip extends StatelessWidget { color: Theme.of(context).colorScheme.primary, decoration: TextDecoration.underline, ), - recognizer: TapGestureRecognizer()..onTap = () => _launchURL(), + recognizer: TapGestureRecognizer() + ..onTap = () => afLaunchUrlString(url), ), TextSpan( text: LocaleKeys.settings_menu_selfHostEnd.tr(), @@ -238,15 +239,6 @@ class AppFlowySelfhostTip extends StatelessWidget { ), ); } - - Future _launchURL() async { - final uri = Uri.parse(url); - if (await canLaunchUrl(uri)) { - await launchUrl(uri); - } else { - Log.error("Could not launch $url"); - } - } } @visibleForTesting diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_file_import_appflowy_data_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_file_import_appflowy_data_view.dart index e3faa32a93..20498752b9 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_file_import_appflowy_data_view.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_file_import_appflowy_data_view.dart @@ -1,16 +1,16 @@ +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; + +import 'package:appflowy/core/helpers/url_launcher.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/workspace/application/settings/setting_file_importer_bloc.dart'; import 'package:appflowy/workspace/presentation/home/toast.dart'; -import 'package:appflowy_backend/log.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/file_picker/file_picker_service.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/gestures.dart'; -import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:fluttertoast/fluttertoast.dart'; -import 'package:url_launcher/url_launcher.dart'; class ImportAppFlowyData extends StatefulWidget { const ImportAppFlowyData({super.key}); @@ -92,22 +92,14 @@ class AppFlowyDataImportTip extends StatelessWidget { color: Theme.of(context).colorScheme.primary, decoration: TextDecoration.underline, ), - recognizer: TapGestureRecognizer()..onTap = () => _launchURL(), + recognizer: TapGestureRecognizer() + ..onTap = () => afLaunchUrlString(url), ), ], ), ), ); } - - Future _launchURL() async { - final uri = Uri.parse(url); - if (await canLaunchUrl(uri)) { - await launchUrl(uri); - } else { - Log.error("Could not launch $url"); - } - } } class ImportAppFlowyDataButton extends StatefulWidget { diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_supabase_cloud.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_supabase_cloud.dart index dbfaa3f7f6..c014cdf516 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_supabase_cloud.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_supabase_cloud.dart @@ -1,3 +1,8 @@ +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +import 'package:appflowy/core/helpers/url_launcher.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/workspace/application/settings/supabase_cloud_setting_bloc.dart'; import 'package:appflowy/workspace/application/settings/supabase_cloud_urls_bloc.dart'; @@ -5,7 +10,6 @@ import 'package:appflowy/workspace/presentation/home/toast.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/_restart_app_button.dart'; import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; import 'package:appflowy_backend/dispatch/dispatch.dart'; -import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-user/user_setting.pb.dart'; import 'package:appflowy_result/appflowy_result.dart'; @@ -14,11 +18,7 @@ import 'package:flowy_infra/size.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/widget/error_page.dart'; import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; -import 'package:flutter/gestures.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:url_launcher/url_launcher.dart'; class SettingSupabaseCloudView extends StatelessWidget { const SettingSupabaseCloudView({required this.restartAppFlowy, super.key}); @@ -330,7 +330,8 @@ class SupabaseSelfhostTip extends StatelessWidget { color: Theme.of(context).colorScheme.primary, decoration: TextDecoration.underline, ), - recognizer: TapGestureRecognizer()..onTap = () => _launchURL(), + recognizer: TapGestureRecognizer() + ..onTap = () => afLaunchUrlString(url), ), TextSpan( text: LocaleKeys.settings_menu_selfHostEnd.tr(), @@ -341,13 +342,4 @@ class SupabaseSelfhostTip extends StatelessWidget { ), ); } - - Future _launchURL() async { - final uri = Uri.parse(url); - if (await canLaunchUrl(uri)) { - await launchUrl(uri); - } else { - Log.error("Could not launch $url"); - } - } } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_file_customize_location_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_file_customize_location_view.dart index 0116690dbb..6f2a3c556f 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_file_customize_location_view.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_file_customize_location_view.dart @@ -1,5 +1,9 @@ import 'dart:io'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +import 'package:appflowy/core/helpers/url_launcher.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/workspace/application/settings/settings_location_cubit.dart'; import 'package:easy_localization/easy_localization.dart'; @@ -8,11 +12,8 @@ import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/style_widget/hover.dart'; import 'package:flowy_infra_ui/widget/buttons/secondary_button.dart'; import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:styled_widget/styled_widget.dart'; -import 'package:url_launcher/url_launcher.dart'; import '../../../../generated/locale_keys.g.dart'; import '../../../../startup/startup.dart'; @@ -234,9 +235,7 @@ class _OpenStorageButton extends StatelessWidget { ), onPressed: () async { final uri = Directory(usingPath).uri; - if (await canLaunchUrl(uri)) { - await launchUrl(uri); - } + await afLaunchUrl(uri, context: context); }, ); } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/theme_upload/theme_upload_learn_more_button.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/theme_upload/theme_upload_learn_more_button.dart index e386aa3ebc..628232bd71 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/theme_upload/theme_upload_learn_more_button.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/theme_upload/theme_upload_learn_more_button.dart @@ -1,3 +1,6 @@ +import 'package:flutter/material.dart'; + +import 'package:appflowy/core/helpers/url_launcher.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/theme_upload/theme_upload_view.dart'; import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; @@ -5,14 +8,12 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/widget/buttons/secondary_button.dart'; import 'package:flowy_infra_ui/widget/error_page.dart'; -import 'package:flutter/material.dart'; -import 'package:url_launcher/url_launcher.dart'; class ThemeUploadLearnMoreButton extends StatelessWidget { const ThemeUploadLearnMoreButton({super.key}); static const learnMoreURL = - 'https://appflowy.gitbook.io/docs/essential-documentation/themes'; + 'https://docs.appflowy.io/docs/appflowy/product/themes'; @override Widget build(BuildContext context) { @@ -30,27 +31,29 @@ class ThemeUploadLearnMoreButton extends StatelessWidget { ), onPressed: () async { final uri = Uri.parse(learnMoreURL); - if (await canLaunchUrl(uri)) { - await launchUrl(uri); - } else { - if (context.mounted) { - await Dialogs.show( - context, - child: FlowyDialog( - child: FlowyErrorPage.message( - LocaleKeys - .settings_appearance_themeUpload_urlUploadFailure - .tr() - .replaceAll( - '{}', - uri.toString(), - ), - howToFix: LocaleKeys.errorDialog_howToFixFallback.tr(), + await afLaunchUrl( + uri, + context: context, + onFailure: (_) async { + if (context.mounted) { + await Dialogs.show( + context, + child: FlowyDialog( + child: FlowyErrorPage.message( + LocaleKeys + .settings_appearance_themeUpload_urlUploadFailure + .tr() + .replaceAll( + '{}', + uri.toString(), + ), + howToFix: LocaleKeys.errorDialog_howToFixFallback.tr(), + ), ), - ), - ); - } - } + ); + } + }, + ); }, ), ), diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/float_bubble/question_bubble.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/float_bubble/question_bubble.dart index 0190e74a47..e72dfa098c 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/float_bubble/question_bubble.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/float_bubble/question_bubble.dart @@ -1,3 +1,7 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +import 'package:appflowy/core/helpers/url_launcher.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/startup/tasks/rust_sdk.dart'; @@ -10,11 +14,8 @@ import 'package:flowy_infra/size.dart'; import 'package:flowy_infra_ui/style_widget/button.dart'; import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flowy_infra_ui/widget/spacing.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:styled_widget/styled_widget.dart'; -import 'package:url_launcher/url_launcher.dart'; class QuestionBubble extends StatelessWidget { const QuestionBubble({super.key}); @@ -86,26 +87,26 @@ class _BubbleActionListState extends State { if (action is BubbleActionWrapper) { switch (action.inner) { case BubbleAction.whatsNews: - _launchURL("https://www.appflowy.io/what-is-new"); + afLaunchUrlString("https://www.appflowy.io/what-is-new"); break; case BubbleAction.help: - _launchURL("https://discord.gg/9Q2xaN37tV"); + afLaunchUrlString("https://discord.gg/9Q2xaN37tV"); break; case BubbleAction.debug: _DebugToast().show(); break; case BubbleAction.shortcuts: - _launchURL( - "https://appflowy.gitbook.io/docs/essential-documentation/shortcuts", + afLaunchUrlString( + "https://docs.appflowy.io/docs/appflowy/product/shortcuts", ); break; case BubbleAction.markdown: - _launchURL( - "https://appflowy.gitbook.io/docs/essential-documentation/markdown", + afLaunchUrlString( + "https://docs.appflowy.io/docs/appflowy/product/markdown", ); break; case BubbleAction.github: - _launchURL( + afLaunchUrlString( 'https://github.com/AppFlowy-IO/AppFlowy/issues/new/choose', ); break; @@ -116,15 +117,6 @@ class _BubbleActionListState extends State { }, ); } - - void _launchURL(String url) async { - final uri = Uri.parse(url); - if (await canLaunchUrl(uri)) { - await launchUrl(uri); - } else { - throw 'Could not launch $url'; - } - } } class _DebugToast { diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index d105849aa6..f07c4972a9 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -9,6 +9,7 @@ "title": "Title", "youCanAlso": "You can also", "and": "and", + "failedToOpenUrl": "Failed to open url: {}", "blockActions": { "addBelowTooltip": "Click to add below", "addAboveCmd": "Alt+click",