mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
Merge branch 'main' into workspace-rename-no-icon
This commit is contained in:
12
.github/workflows/flutter_ci.yaml
vendored
12
.github/workflows/flutter_ci.yaml
vendored
@ -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:
|
||||
|
50
frontend/appflowy_flutter/lib/core/helpers/url_launcher.dart
Normal file
50
frontend/appflowy_flutter/lib/core/helpers/url_launcher.dart
Normal 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");
|
||||
}
|
||||
}
|
@ -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(),
|
||||
|
@ -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',
|
||||
);
|
||||
},
|
||||
|
@ -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(),
|
||||
),
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -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({
|
||||
|
@ -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,
|
||||
|
@ -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) {
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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),
|
||||
|
@ -58,7 +58,7 @@ class GridChecklistCellState extends GridCellState<EditableChecklistCell> {
|
||||
widget.databaseController,
|
||||
widget.cellContext,
|
||||
).as(),
|
||||
)..add(const ChecklistCellEvent.initial());
|
||||
);
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
|
@ -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,
|
||||
|
@ -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(
|
||||
|
@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -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();
|
||||
},
|
||||
|
@ -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();
|
||||
|
@ -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,
|
||||
);
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
25
frontend/appflowy_flutter/lib/shared/feature_flags.dart
Normal file
25
frontend/appflowy_flutter/lib/shared/feature_flags.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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',
|
||||
|
@ -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 {
|
||||
|
@ -16,6 +16,7 @@ enum SettingsPage {
|
||||
notifications,
|
||||
cloud,
|
||||
shortcuts,
|
||||
member,
|
||||
}
|
||||
|
||||
class SettingsDialogBloc
|
||||
|
@ -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'),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
@ -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 {
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
@ -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');
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
@ -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,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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(),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
|
@ -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 {
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
10
frontend/resources/flowy_icons/24x/invite_member_link.svg
Normal file
10
frontend/resources/flowy_icons/24x/invite_member_link.svg
Normal 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 |
@ -848,10 +848,10 @@
|
||||
"addToColumnBottomTooltip": "أضف بطاقة جديدة في الأسفل",
|
||||
"renameColumn": "إعادة تسمية",
|
||||
"hideColumn": "اخفاء",
|
||||
"groupActions": "إجراءات المجموعة",
|
||||
"newGroup": "مجموعة جديدة",
|
||||
"deleteColumn": "مسح",
|
||||
"deleteColumnConfirmation": "سيؤدي هذا إلى حذف هذه المجموعة وجميع البطاقات الموجودة فيها. هل أنت متأكد من الاستمرار؟"
|
||||
"deleteColumnConfirmation": "سيؤدي هذا إلى حذف هذه المجموعة وجميع البطاقات الموجودة فيها. هل أنت متأكد من الاستمرار؟",
|
||||
"groupActions": "إجراءات المجموعة"
|
||||
},
|
||||
"hiddenGroupSection": {
|
||||
"sectionTitle": "المجموعات المخفية",
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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"
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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"
|
||||
|
@ -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": "Нет файлов журналов"
|
||||
}
|
||||
|
@ -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": "私密组",
|
||||
|
@ -894,10 +894,10 @@
|
||||
"addToColumnBottomTooltip": "在底端新增卡片",
|
||||
"renameColumn": "重新命名",
|
||||
"hideColumn": "隱藏",
|
||||
"groupActions": "群組操作",
|
||||
"newGroup": "新增群組",
|
||||
"deleteColumn": "刪除",
|
||||
"deleteColumnConfirmation": "這將刪除此群組及其中所有卡片。\n您確定要繼續嗎?"
|
||||
"deleteColumnConfirmation": "這將刪除此群組及其中所有卡片。\n您確定要繼續嗎?",
|
||||
"groupActions": "群組操作"
|
||||
},
|
||||
"hiddenGroupSection": {
|
||||
"sectionTitle": "隱藏的群組",
|
||||
|
@ -594,13 +594,15 @@ impl DatabaseEditor {
|
||||
index: index as i32,
|
||||
};
|
||||
let notified_changeset = DatabaseFieldChangesetPB {
|
||||
view_id: params.view_id,
|
||||
view_id: params.view_id.clone(),
|
||||
inserted_fields: vec![insert_field],
|
||||
deleted_fields: vec![delete_field],
|
||||
updated_fields: vec![],
|
||||
};
|
||||
|
||||
self.notify_did_update_database(notified_changeset).await?;
|
||||
send_notification(¶ms.view_id, DatabaseNotification::DidUpdateFields)
|
||||
.payload(notified_changeset)
|
||||
.send();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
@ -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")]
|
||||
|
Reference in New Issue
Block a user