Merge branch 'main' into workspace-rename-no-icon

This commit is contained in:
Zack Fu Zi Xiang
2024-03-04 09:29:03 +08:00
56 changed files with 1388 additions and 409 deletions

View File

@ -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:

View File

@ -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<bool> 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<void> 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");
}
}

View File

@ -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(),

View File

@ -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',
);
},

View File

@ -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(),
),

View File

@ -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<ChecklistCellEvent, ChecklistCellState> {
),
super(ChecklistCellState.initial(cellController)) {
_dispatch();
_startListening();
}
final ChecklistCellController cellController;
@ -46,9 +47,6 @@ class ChecklistCellBloc extends Bloc<ChecklistCellEvent, ChecklistCellState> {
on<ChecklistCellEvent>(
(event, emit) async {
await event.when(
initial: () {
_startListening();
},
didReceiveOptions: (data) {
if (data == null) {
emit(
@ -71,8 +69,8 @@ class ChecklistCellBloc extends Bloc<ChecklistCellEvent, ChecklistCellState> {
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<ChecklistCellEvent, ChecklistCellState> {
(err) => Log.error(err),
);
},
deleteTask: (option) async {
await _deleteOption([option]);
deleteTask: (id) async {
await _deleteOption([id]);
},
);
},
@ -102,21 +100,17 @@ class ChecklistCellBloc extends Bloc<ChecklistCellEvent, ChecklistCellState> {
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<void> _deleteOption(List<SelectOptionPB> options) async {
final result = await _checklistCellService.delete(
optionIds: options.map((e) => e.id).toList(),
);
Future<void> _deleteOption(List<String> 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<ChecklistSelectOption> _makeChecklistSelectOptions(
if (data == null) {
return [];
}
final List<ChecklistSelectOption> options = [];
final List<SelectOptionPB> 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();
}

View File

@ -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();

View File

@ -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({

View File

@ -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,

View File

@ -42,7 +42,7 @@ class _ChecklistCellState extends State<ChecklistCardCell> {
widget.databaseController,
widget.cellContext,
).as(),
)..add(const ChecklistCellEvent.initial());
);
},
child: BlocBuilder<ChecklistCellBloc, ChecklistCellState>(
builder: (context, state) {

View File

@ -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);
}
}

View File

@ -64,14 +64,17 @@ class _ChecklistItemsState extends State<ChecklistItems> {
}
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<ChecklistItems> {
],
),
),
const VSpace(4),
const VSpace(2.0),
...children,
ChecklistItemControl(cellNotifer: widget.cellContainerNotifier),
],
@ -136,7 +139,7 @@ class ChecklistItemControl extends StatelessWidget {
.read<ChecklistCellBloc>()
.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),

View File

@ -58,7 +58,7 @@ class GridChecklistCellState extends GridCellState<EditableChecklistCell> {
widget.databaseController,
widget.cellContext,
).as(),
)..add(const ChecklistCellEvent.initial());
);
@override
void dispose() {

View File

@ -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,

View File

@ -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(

View File

@ -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<ChecklistCellEditor> createState() => _GridChecklistCellState();
State<ChecklistCellEditor> createState() => _ChecklistCellEditorState();
}
class _GridChecklistCellState extends State<ChecklistCellEditor> {
class _ChecklistCellEditorState extends State<ChecklistCellEditor> {
/// Focus node for the new task text field
late final FocusNode newTaskFocusNode;
@ -56,18 +57,14 @@ class _GridChecklistCellState extends State<ChecklistCellEditor> {
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<ChecklistCellEditor> {
/// 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<ChecklistSelectOption> options;
final VoidCallback onUpdateTask;
@override
State<ChecklistItemList> createState() => _ChecklistItemListState();
}
class _ChecklistItemListState extends State<ChecklistItemList> {
@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<ChecklistItemList> {
}
}
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<ChecklistItem> {
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<ChecklistCellBloc>()
.add(ChecklistCellEvent.selectTask(widget.task.data.id)),
),
_DeleteTaskIntent: CallbackAction<_DeleteTaskIntent>(
onInvoke: (_DeleteTaskIntent intent) => context
.read<ChecklistCellBloc>()
.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<ChecklistItem> {
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<ChecklistCellBloc>().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<ChecklistCellBloc>().add(
ChecklistCellEvent.deleteTask(widget.task.data),
ChecklistCellEvent.deleteTask(widget.task.data.id),
),
),
],
@ -276,7 +326,7 @@ class _ChecklistItemState extends State<ChecklistItem> {
context.read<ChecklistCellBloc>().add(
ChecklistCellEvent.updateTaskName(
widget.task.data,
description.trim(),
description,
),
);
}

View File

@ -159,7 +159,7 @@ class _ChecklistItemState extends State<_ChecklistItem> {
borderRadius: BorderRadius.circular(22),
onTap: () => context
.read<ChecklistCellBloc>()
.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<ChecklistCellBloc>().add(
ChecklistCellEvent.deleteTask(widget.task.data),
ChecklistCellEvent.deleteTask(widget.task.data.id),
);
context.pop();
},

View File

@ -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<DocumentHeaderNodeWidget> {
DocumentHeaderBlockKeys.coverDetails: coverDetails,
DocumentHeaderBlockKeys.icon:
widget.node.attributes[DocumentHeaderBlockKeys.icon],
CustomImageBlockKeys.imageType: '1',
};
if (cover != null) {
attributes[DocumentHeaderBlockKeys.coverType] = cover.$1.toString();

View File

@ -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,
);
}

View File

@ -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<void> 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);
}

View File

@ -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<EditorState>();
if (editorState.selection == null) {
safeLaunchUrl(href);
afLaunchUrlString(href);
return;
}

View File

@ -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;
}
}
}

View File

@ -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',

View File

@ -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<void> _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 {

View File

@ -16,6 +16,7 @@ enum SettingsPage {
notifications,
cloud,
shortcuts,
member,
}
class SettingsDialogBloc

View File

@ -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<WorkspaceFailedScreen> {
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<WorkspaceFailedScreen> {
title: LocaleKeys.workspace_errorActions_reachOut.tr(),
height: 40,
onPressed: () =>
safeLaunchUrl('https://discord.gg/JucBXeU2FE'),
afLaunchUrlString('https://discord.gg/JucBXeU2FE'),
),
),
],

View File

@ -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 {

View File

@ -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();
}

View File

@ -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<WorkspaceMemberEvent, WorkspaceMemberState> {
WorkspaceMemberBloc({
required this.userProfile,
}) : super(WorkspaceMemberState.initial()) {
on<WorkspaceMemberEvent>((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<List<WorkspaceMemberPB>> _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<WorkspaceMemberPB> 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<void> _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<void> _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<void> _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<WorkspaceMemberPB> members,
@Default(AFRolePB.Guest) AFRolePB myRole,
}) = _WorkspaceMemberState;
factory WorkspaceMemberState.initial() => const WorkspaceMemberState();
}

View File

@ -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<WorkspaceMemberBloc>(
create: (context) => WorkspaceMemberBloc(userProfile: userProfile)
..add(
const WorkspaceMemberEvent.getWorkspaceMembers(),
),
child: BlocBuilder<WorkspaceMemberBloc, WorkspaceMemberState>(
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<WorkspaceMemberBloc>()
.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<WorkspaceMemberPB> 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<WorkspaceMemberBloc>().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<WorkspaceMemberBloc>().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');
}
}

View File

@ -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<void> _launchURL() async {
final uri = Uri.parse(url);
if (await canLaunchUrl(uri)) {
await launchUrl(uri);
} else {
Log.error("Could not launch $url");
}
}
}
@visibleForTesting

View File

@ -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<void> _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 {

View File

@ -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<void> _launchURL() async {
final uri = Uri.parse(url);
if (await canLaunchUrl(uri)) {
await launchUrl(uri);
} else {
Log.error("Could not launch $url");
}
}
}

View File

@ -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);
},
);
}

View File

@ -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,
),
],
),
);
}
}

View File

@ -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(),
),
),
),
);
}
}
);
}
},
);
},
),
),

View File

@ -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<BubbleActionList> {
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<BubbleActionList> {
},
);
}
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 {

View File

@ -161,15 +161,16 @@ class PopoverState extends State<Popover> {
),
);
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);

View File

@ -14,6 +14,14 @@ abstract class FlowyResult<S, F> {
bool isFailure();
S? toNullable();
void onSuccess(
void Function(S s) onSuccess,
);
void onFailure(
void Function(F f) onFailure,
);
}
class FlowySuccess<S, F> implements FlowyResult<S, F> {
@ -64,6 +72,14 @@ class FlowySuccess<S, F> implements FlowyResult<S, F> {
S? toNullable() {
return _value;
}
@override
void onSuccess(void Function(S success) onSuccess) {
onSuccess(_value);
}
@override
void onFailure(void Function(F failure) onFailure) {}
}
class FlowyFailure<S, F> implements FlowyResult<S, F> {
@ -114,4 +130,12 @@ class FlowyFailure<S, F> implements FlowyResult<S, F> {
S? toNullable() {
return null;
}
@override
void onSuccess(void Function(S success) onSuccess) {}
@override
void onFailure(void Function(F failure) onFailure) {
onFailure(_error);
}
}

View File

@ -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,

View File

@ -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,
),
),
);

View File

@ -0,0 +1,10 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="link">
<mask id="mask0_1032_7497" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="24" height="24">
<rect id="Bounding box" width="24" height="24" fill="#D9D9D9"/>
</mask>
<g mask="url(#mask0_1032_7497)">
<path id="link_2" d="M11 17H7C5.61667 17 4.4375 16.5125 3.4625 15.5375C2.4875 14.5625 2 13.3833 2 12C2 10.6167 2.4875 9.4375 3.4625 8.4625C4.4375 7.4875 5.61667 7 7 7H11V9H7C6.16667 9 5.45833 9.29167 4.875 9.875C4.29167 10.4583 4 11.1667 4 12C4 12.8333 4.29167 13.5417 4.875 14.125C5.45833 14.7083 6.16667 15 7 15H11V17ZM8 13V11H16V13H8ZM13 17V15H17C17.8333 15 18.5417 14.7083 19.125 14.125C19.7083 13.5417 20 12.8333 20 12C20 11.1667 19.7083 10.4583 19.125 9.875C18.5417 9.29167 17.8333 9 17 9H13V7H17C18.3833 7 19.5625 7.4875 20.5375 8.4625C21.5125 9.4375 22 10.6167 22 12C22 13.3833 21.5125 14.5625 20.5375 15.5375C19.5625 16.5125 18.3833 17 17 17H13Z" fill="#1C1B1F"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1019 B

View File

@ -848,10 +848,10 @@
"addToColumnBottomTooltip": "أضف بطاقة جديدة في الأسفل",
"renameColumn": "إعادة تسمية",
"hideColumn": "اخفاء",
"groupActions": "إجراءات المجموعة",
"newGroup": "مجموعة جديدة",
"deleteColumn": "مسح",
"deleteColumnConfirmation": "سيؤدي هذا إلى حذف هذه المجموعة وجميع البطاقات الموجودة فيها. هل أنت متأكد من الاستمرار؟"
"deleteColumnConfirmation": "سيؤدي هذا إلى حذف هذه المجموعة وجميع البطاقات الموجودة فيها. هل أنت متأكد من الاستمرار؟",
"groupActions": "إجراءات المجموعة"
},
"hiddenGroupSection": {
"sectionTitle": "المجموعات المخفية",

View File

@ -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",

View File

@ -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",
@ -397,7 +398,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",

View File

@ -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"

View File

@ -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",

View File

@ -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",

View File

@ -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"

View File

@ -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": "Восстановить по умолчанию"
@ -870,10 +949,10 @@
"addToColumnBottomTooltip": "Add a new card at the bottom",
"renameColumn": "Переименовать",
"hideColumn": "Скрыть",
"groupActions": "Групповые действия",
"newGroup": "Новая группа",
"deleteColumn": "Удалить",
"deleteColumnConfirmation": "Это удалит колонку и все карточки в ней.\nТочно продолжить?"
"deleteColumnConfirmation": "Это удалит колонку и все карточки в ней.\nТочно продолжить?",
"groupActions": "Групповые действия"
},
"hiddenGroupSection": {
"sectionTitle": "Скрытые группы",
@ -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": "Нет файлов журналов"
}

View File

@ -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": "私密组",

View File

@ -894,10 +894,10 @@
"addToColumnBottomTooltip": "在底端新增卡片",
"renameColumn": "重新命名",
"hideColumn": "隱藏",
"groupActions": "群組操作",
"newGroup": "新增群組",
"deleteColumn": "刪除",
"deleteColumnConfirmation": "這將刪除此群組及其中所有卡片。\n您確定要繼續嗎"
"deleteColumnConfirmation": "這將刪除此群組及其中所有卡片。\n您確定要繼續嗎",
"groupActions": "群組操作"
},
"hiddenGroupSection": {
"sectionTitle": "隱藏的群組",

View File

@ -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(&params.view_id, DatabaseNotification::DidUpdateFields)
.payload(notified_changeset)
.send();
}
Ok(())

View File

@ -182,16 +182,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")]