Compare commits
12
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e527a1843e | ||
|
|
3f8eb70ff3 | ||
|
|
bd7976d005 | ||
|
|
4e1532af3e | ||
|
|
97999aee44 | ||
|
|
6785104c3a | ||
|
|
193c8242e2 | ||
|
|
59eda5e038 | ||
|
|
3c8de8a52c | ||
|
|
d38a5f38e3 | ||
|
|
fe4f85a597 | ||
|
|
0a1af4e61f |
No files matched your search
@@ -1,4 +1,18 @@
|
||||
# Release Notes
|
||||
## Version 0.7.3 - 07/11/2024
|
||||
### New Features
|
||||
- Enable custom URLs for published pages
|
||||
- Support toggling headings
|
||||
- Create a subpage by typing in the document
|
||||
- Turn selected blocks into a subpage
|
||||
- Add a manual date picker for the Date property
|
||||
|
||||
### Bug Fixes
|
||||
- Fixed an issue where the workspace owner was unable to delete spaces created by others
|
||||
- Fixed cursor height inconsistencies with text height
|
||||
- Fixed editing issues in Kanban cards
|
||||
- Fixed an issue preventing images or files from being dropped into empty paragraphs
|
||||
|
||||
## Version 0.7.2 - 22/10/2024
|
||||
### New Features
|
||||
- Copy link to block
|
||||
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import '../../shared/util.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('Block option interaction tests', () {
|
||||
testWidgets('has correct block selection on tap option button',
|
||||
(tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
|
||||
// We edit the document by entering some characters, to ensure the document has focus
|
||||
await tester.editor.updateSelection(
|
||||
Selection.collapsed(Position(path: [2])),
|
||||
);
|
||||
|
||||
// Insert character 'a' three times - easy to identify
|
||||
await tester.ime.insertText('aaa');
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final editorState = tester.editor.getCurrentEditorState();
|
||||
final node = editorState.getNodeAtPath([2]);
|
||||
expect(node?.delta?.toPlainText(), startsWith('aaa'));
|
||||
|
||||
final multiSelection = Selection(
|
||||
start: Position(path: [2], offset: 3),
|
||||
end: Position(path: [4], offset: 40),
|
||||
);
|
||||
|
||||
// Select multiple items
|
||||
await tester.editor.updateSelection(multiSelection);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Press the block option menu
|
||||
await tester.editor.hoverAndClickOptionMenuButton([2]);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Expect the selection to be Block type and not have changed
|
||||
expect(editorState.selectionType, SelectionType.block);
|
||||
expect(editorState.selection, multiSelection);
|
||||
});
|
||||
});
|
||||
}
|
||||
+2
-3
@@ -8,6 +8,7 @@ import 'document_with_file_test.dart' as document_with_file_test;
|
||||
import 'document_with_image_block_test.dart' as document_with_image_block_test;
|
||||
import 'document_with_multi_image_block_test.dart'
|
||||
as document_with_multi_image_block_test;
|
||||
import 'document_block_option_test.dart' as document_block_option_test;
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
@@ -19,7 +20,5 @@ void main() {
|
||||
document_more_actions_test.main();
|
||||
document_with_file_test.main();
|
||||
document_shortcuts_test.main();
|
||||
|
||||
// Disable subPage test temporarily, enable it in version 0.7.2
|
||||
// document_sub_page_test.main();
|
||||
document_block_option_test.main();
|
||||
}
|
||||
+129
-105
@@ -13,6 +13,7 @@ import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/remi
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:appflowy_result/appflowy_result.dart';
|
||||
import 'package:calendar_view/calendar_view.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:easy_localization/easy_localization.dart'
|
||||
@@ -54,59 +55,18 @@ class DateCellEditorBloc
|
||||
didReceiveCellUpdate: (DateCellDataPB? cellData) {
|
||||
final dateCellData = DateCellData.fromPB(cellData);
|
||||
|
||||
ReminderOption option = state.reminderOption;
|
||||
final cellReminderId = dateCellData.reminderId;
|
||||
ReminderOption reminderOption = ReminderOption.none;
|
||||
|
||||
if (dateCellData.dateTime != null &&
|
||||
state.reminderId.isEmpty &&
|
||||
state.reminderOption != ReminderOption.none &&
|
||||
cellReminderId.isNotEmpty) {
|
||||
final date = state.reminderOption.withoutTime
|
||||
? dateCellData.dateTime!.withoutTime
|
||||
: dateCellData.dateTime!;
|
||||
|
||||
// Add Reminder
|
||||
_reminderBloc.add(
|
||||
ReminderEvent.addById(
|
||||
reminderId: dateCellData.reminderId,
|
||||
objectId: cellController.viewId,
|
||||
meta: {
|
||||
ReminderMetaKeys.includeTime: true.toString(),
|
||||
ReminderMetaKeys.rowId: cellController.rowId,
|
||||
},
|
||||
scheduledAt: Int64(
|
||||
state.reminderOption
|
||||
.getNotificationDateTime(date)
|
||||
.millisecondsSinceEpoch ~/
|
||||
1000,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (cellReminderId.isNotEmpty && dateCellData.dateTime != null) {
|
||||
if (option.requiresNoTime && dateCellData.includeTime) {
|
||||
option = ReminderOption.atTimeOfEvent;
|
||||
} else if (!option.withoutTime && !dateCellData.includeTime) {
|
||||
option = ReminderOption.onDayOfEvent;
|
||||
if (dateCellData.reminderId.isNotEmpty &&
|
||||
dateCellData.dateTime != null) {
|
||||
final reminder = _reminderBloc.state.reminders
|
||||
.firstWhereOrNull((r) => r.id == dateCellData.reminderId);
|
||||
if (reminder != null) {
|
||||
reminderOption = ReminderOption.fromDateDifference(
|
||||
dateCellData.dateTime!,
|
||||
reminder.scheduledAt.toDateTime(),
|
||||
);
|
||||
}
|
||||
|
||||
final date = option.withoutTime
|
||||
? dateCellData.dateTime!.withoutTime
|
||||
: dateCellData.dateTime!;
|
||||
|
||||
final scheduledAt = option.getNotificationDateTime(date);
|
||||
|
||||
// Update Reminder
|
||||
_reminderBloc.add(
|
||||
ReminderEvent.update(
|
||||
ReminderUpdate(
|
||||
id: dateCellData.reminderId,
|
||||
scheduledAt: scheduledAt,
|
||||
includeTime: true,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
emit(
|
||||
@@ -116,7 +76,7 @@ class DateCellEditorBloc
|
||||
includeTime: dateCellData.includeTime,
|
||||
isRange: dateCellData.isRange,
|
||||
reminderId: dateCellData.reminderId,
|
||||
reminderOption: option,
|
||||
reminderOption: reminderOption,
|
||||
),
|
||||
);
|
||||
},
|
||||
@@ -158,81 +118,55 @@ class DateCellEditorBloc
|
||||
|
||||
await _clearDate();
|
||||
},
|
||||
setReminderOption: (
|
||||
ReminderOption option,
|
||||
) async {
|
||||
if (option == ReminderOption.none && state.reminderId.isNotEmpty) {
|
||||
_reminderBloc.add(
|
||||
ReminderEvent.remove(reminderId: state.reminderId),
|
||||
);
|
||||
await _updateDateData(reminderId: "");
|
||||
} else if (state.reminderId.isEmpty) {
|
||||
// New Reminder
|
||||
final reminderId = nanoid();
|
||||
await _updateDateData(
|
||||
reminderId: reminderId,
|
||||
date: state.dateTime ?? DateTime.now().withoutTime,
|
||||
);
|
||||
} else if (state.dateTime != null) {
|
||||
// Update reminder
|
||||
final scheduledAt =
|
||||
option.getNotificationDateTime(state.dateTime!);
|
||||
_reminderBloc.add(
|
||||
ReminderEvent.update(
|
||||
ReminderUpdate(
|
||||
id: state.reminderId,
|
||||
scheduledAt: scheduledAt,
|
||||
includeTime: true,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
removeReminder: () async {
|
||||
await _updateDateData(reminderId: "");
|
||||
setReminderOption: (ReminderOption option) async {
|
||||
await _setReminderOption(option);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _updateDateData({
|
||||
Future<FlowyResult<void, FlowyError>> _updateDateData({
|
||||
DateTime? date,
|
||||
DateTime? endDate,
|
||||
String? reminderId,
|
||||
bool updateReminderIfNecessary = true,
|
||||
}) async {
|
||||
final result = await _dateCellBackendService.update(
|
||||
date: date,
|
||||
endDate: endDate,
|
||||
reminderId: reminderId,
|
||||
);
|
||||
result.onFailure(Log.error);
|
||||
if (updateReminderIfNecessary) {
|
||||
result.onSuccess((_) => _updateReminderIfNecessary(date));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
Future<void> _updateIsRange(
|
||||
bool isRange,
|
||||
DateTime? dateTime,
|
||||
DateTime? endDateTime,
|
||||
) async {
|
||||
final result = await _dateCellBackendService.update(
|
||||
date: dateTime,
|
||||
endDate: endDateTime,
|
||||
isRange: isRange,
|
||||
);
|
||||
result.onFailure(Log.error);
|
||||
) {
|
||||
return _dateCellBackendService
|
||||
.update(
|
||||
date: dateTime,
|
||||
endDate: endDateTime,
|
||||
isRange: isRange,
|
||||
)
|
||||
.fold((s) => _updateReminderIfNecessary(dateTime), Log.error);
|
||||
}
|
||||
|
||||
Future<void> _updateIncludeTime(
|
||||
bool includeTime,
|
||||
DateTime? dateTime,
|
||||
DateTime? endDateTime,
|
||||
) async {
|
||||
final result = await _dateCellBackendService.update(
|
||||
date: dateTime,
|
||||
endDate: endDateTime,
|
||||
includeTime: includeTime,
|
||||
);
|
||||
result.onFailure(Log.error);
|
||||
) {
|
||||
return _dateCellBackendService
|
||||
.update(
|
||||
date: dateTime,
|
||||
endDate: endDateTime,
|
||||
includeTime: includeTime,
|
||||
)
|
||||
.fold((s) => _updateReminderIfNecessary(dateTime), Log.error);
|
||||
}
|
||||
|
||||
Future<void> _clearDate() async {
|
||||
@@ -240,6 +174,98 @@ class DateCellEditorBloc
|
||||
result.onFailure(Log.error);
|
||||
}
|
||||
|
||||
Future<void> _setReminderOption(ReminderOption option) async {
|
||||
if (state.reminderId.isEmpty) {
|
||||
if (option == ReminderOption.none) {
|
||||
// do nothing
|
||||
return;
|
||||
}
|
||||
// if no date, fill it first
|
||||
final fillerDateTime =
|
||||
state.includeTime ? DateTime.now() : DateTime.now().withoutTime;
|
||||
if (state.dateTime == null) {
|
||||
final result = await _updateDateData(
|
||||
date: fillerDateTime,
|
||||
endDate: state.isRange ? fillerDateTime : null,
|
||||
updateReminderIfNecessary: false,
|
||||
);
|
||||
// return if filling date is unsuccessful
|
||||
if (result.isFailure) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// create a reminder
|
||||
final reminderId = nanoid();
|
||||
await _updateCellReminderId(reminderId);
|
||||
final dateTime = state.dateTime ?? fillerDateTime;
|
||||
_reminderBloc.add(
|
||||
ReminderEvent.addById(
|
||||
reminderId: reminderId,
|
||||
objectId: cellController.viewId,
|
||||
meta: {
|
||||
ReminderMetaKeys.includeTime: state.includeTime.toString(),
|
||||
ReminderMetaKeys.rowId: cellController.rowId,
|
||||
},
|
||||
scheduledAt: Int64(
|
||||
option.getNotificationDateTime(dateTime).millisecondsSinceEpoch ~/
|
||||
1000,
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
if (option == ReminderOption.none) {
|
||||
// remove reminder from reminder bloc and cell data
|
||||
_reminderBloc.add(ReminderEvent.remove(reminderId: state.reminderId));
|
||||
await _updateCellReminderId("");
|
||||
} else {
|
||||
// Update reminder
|
||||
final scheduledAt = option.getNotificationDateTime(state.dateTime!);
|
||||
_reminderBloc.add(
|
||||
ReminderEvent.update(
|
||||
ReminderUpdate(
|
||||
id: state.reminderId,
|
||||
scheduledAt: scheduledAt,
|
||||
includeTime: state.includeTime,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _updateCellReminderId(
|
||||
String reminderId,
|
||||
) async {
|
||||
final result = await _dateCellBackendService.update(
|
||||
reminderId: reminderId,
|
||||
);
|
||||
result.onFailure(Log.error);
|
||||
}
|
||||
|
||||
void _updateReminderIfNecessary(
|
||||
DateTime? dateTime,
|
||||
) {
|
||||
if (state.reminderId.isEmpty ||
|
||||
state.reminderOption == ReminderOption.none ||
|
||||
dateTime == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final scheduledAt = state.reminderOption.getNotificationDateTime(dateTime);
|
||||
|
||||
// Update Reminder
|
||||
_reminderBloc.add(
|
||||
ReminderEvent.update(
|
||||
ReminderUpdate(
|
||||
id: state.reminderId,
|
||||
scheduledAt: scheduledAt,
|
||||
includeTime: state.includeTime,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String timeFormatPrompt(FlowyError error) {
|
||||
return switch (state.dateTypeOptionPB.timeFormat) {
|
||||
TimeFormatPB.TwelveHour =>
|
||||
@@ -300,7 +326,7 @@ class DateCellEditorBloc
|
||||
typeOptionData: newDateTypeOption.writeToBuffer(),
|
||||
);
|
||||
|
||||
result.fold((_) => {}, (err) => Log.error(err));
|
||||
result.onFailure(Log.error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -338,8 +364,6 @@ class DateCellEditorEvent with _$DateCellEditorEvent {
|
||||
const factory DateCellEditorEvent.setReminderOption(ReminderOption option) =
|
||||
_SetReminderOption;
|
||||
|
||||
const factory DateCellEditorEvent.removeReminder() = _RemoveReminder;
|
||||
|
||||
// date field type options are modified
|
||||
const factory DateCellEditorEvent.setTimeFormat(TimeFormatPB timeFormat) =
|
||||
_SetTimeFormat;
|
||||
|
||||
+76
-52
@@ -55,6 +55,14 @@ class ChecklistRowDetailCell extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _ChecklistRowDetailCellState extends State<ChecklistRowDetailCell> {
|
||||
final phantomTextController = TextEditingController();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
phantomTextController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Align(
|
||||
@@ -69,6 +77,7 @@ class _ChecklistRowDetailCellState extends State<ChecklistRowDetailCell> {
|
||||
),
|
||||
const VSpace(2.0),
|
||||
_ChecklistItems(
|
||||
phantomTextController: phantomTextController,
|
||||
onStartCreatingTaskAfter: (index) {
|
||||
context
|
||||
.read<ChecklistCellBloc>()
|
||||
@@ -80,6 +89,7 @@ class _ChecklistRowDetailCellState extends State<ChecklistRowDetailCell> {
|
||||
onTap: () {
|
||||
final bloc = context.read<ChecklistCellBloc>();
|
||||
if (bloc.state.phantomIndex == null) {
|
||||
phantomTextController.clear();
|
||||
bloc.add(
|
||||
ChecklistCellEvent.updatePhantomIndex(
|
||||
bloc.state.showIncompleteOnly
|
||||
@@ -89,6 +99,13 @@ class _ChecklistRowDetailCellState extends State<ChecklistRowDetailCell> {
|
||||
: bloc.state.tasks.length,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
bloc.add(
|
||||
ChecklistCellEvent.createNewTask(
|
||||
phantomTextController.text,
|
||||
index: bloc.state.phantomIndex,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
@@ -153,9 +170,11 @@ class ProgressAndHideCompleteButton extends StatelessWidget {
|
||||
|
||||
class _ChecklistItems extends StatelessWidget {
|
||||
const _ChecklistItems({
|
||||
required this.phantomTextController,
|
||||
required this.onStartCreatingTaskAfter,
|
||||
});
|
||||
|
||||
final TextEditingController phantomTextController;
|
||||
final void Function(int index) onStartCreatingTaskAfter;
|
||||
|
||||
@override
|
||||
@@ -165,6 +184,7 @@ class _ChecklistItems extends StatelessWidget {
|
||||
_CancelCreatingFromPhantomIntent:
|
||||
CallbackAction<_CancelCreatingFromPhantomIntent>(
|
||||
onInvoke: (_CancelCreatingFromPhantomIntent intent) {
|
||||
phantomTextController.clear();
|
||||
context
|
||||
.read<ChecklistCellBloc>()
|
||||
.add(const ChecklistCellEvent.updatePhantomIndex(null));
|
||||
@@ -240,7 +260,10 @@ class _ChecklistItems extends StatelessWidget {
|
||||
Padding(
|
||||
key: const ValueKey('new_checklist_cell_task'),
|
||||
padding: const EdgeInsets.symmetric(vertical: 2.0),
|
||||
child: PhantomChecklistItem(index: state.phantomIndex!),
|
||||
child: PhantomChecklistItem(
|
||||
index: state.phantomIndex!,
|
||||
textController: phantomTextController,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -268,16 +291,17 @@ class PhantomChecklistItem extends StatefulWidget {
|
||||
const PhantomChecklistItem({
|
||||
super.key,
|
||||
required this.index,
|
||||
required this.textController,
|
||||
});
|
||||
|
||||
final int index;
|
||||
final TextEditingController textController;
|
||||
|
||||
@override
|
||||
State<PhantomChecklistItem> createState() => _PhantomChecklistItemState();
|
||||
}
|
||||
|
||||
class _PhantomChecklistItemState extends State<PhantomChecklistItem> {
|
||||
TextEditingController textController = TextEditingController();
|
||||
final focusNode = FocusNode();
|
||||
|
||||
bool isComposing = false;
|
||||
@@ -285,18 +309,19 @@ class _PhantomChecklistItemState extends State<PhantomChecklistItem> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
textController.addListener(_onTextChanged);
|
||||
widget.textController.addListener(_onTextChanged);
|
||||
focusNode.addListener(_onFocusChanged);
|
||||
WidgetsBinding.instance
|
||||
.addPostFrameCallback((_) => focusNode.requestFocus());
|
||||
}
|
||||
|
||||
void _onTextChanged() =>
|
||||
setState(() => isComposing = !textController.value.composing.isCollapsed);
|
||||
void _onTextChanged() => setState(
|
||||
() => isComposing = !widget.textController.value.composing.isCollapsed,
|
||||
);
|
||||
|
||||
void _onFocusChanged() {
|
||||
if (!focusNode.hasFocus) {
|
||||
textController.clear();
|
||||
widget.textController.clear();
|
||||
Actions.maybeInvoke(
|
||||
context,
|
||||
const _CancelCreatingFromPhantomIntent(),
|
||||
@@ -306,8 +331,7 @@ class _PhantomChecklistItemState extends State<PhantomChecklistItem> {
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
textController.removeListener(_onTextChanged);
|
||||
textController.dispose();
|
||||
widget.textController.removeListener(_onTextChanged);
|
||||
focusNode.removeListener(_onFocusChanged);
|
||||
focusNode.dispose();
|
||||
super.dispose();
|
||||
@@ -319,15 +343,13 @@ class _PhantomChecklistItemState extends State<PhantomChecklistItem> {
|
||||
actions: {
|
||||
_SubmitPhantomTaskIntent: CallbackAction<_SubmitPhantomTaskIntent>(
|
||||
onInvoke: (_SubmitPhantomTaskIntent intent) {
|
||||
if (intent.taskDescription.isNotEmpty) {
|
||||
context.read<ChecklistCellBloc>().add(
|
||||
ChecklistCellEvent.createNewTask(
|
||||
intent.taskDescription,
|
||||
index: intent.index,
|
||||
),
|
||||
);
|
||||
}
|
||||
textController.clear();
|
||||
context.read<ChecklistCellBloc>().add(
|
||||
ChecklistCellEvent.createNewTask(
|
||||
intent.taskDescription,
|
||||
index: intent.index,
|
||||
),
|
||||
);
|
||||
widget.textController.clear();
|
||||
return;
|
||||
},
|
||||
),
|
||||
@@ -342,11 +364,11 @@ class _PhantomChecklistItemState extends State<PhantomChecklistItem> {
|
||||
),
|
||||
child: Center(
|
||||
child: ChecklistCellTextfield(
|
||||
textController: textController,
|
||||
textController: widget.textController,
|
||||
focusNode: focusNode,
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 10,
|
||||
vertical: 10,
|
||||
vertical: 8,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -361,7 +383,7 @@ class _PhantomChecklistItemState extends State<PhantomChecklistItem> {
|
||||
: {
|
||||
const SingleActivator(LogicalKeyboardKey.enter):
|
||||
_SubmitPhantomTaskIntent(
|
||||
taskDescription: textController.text,
|
||||
taskDescription: widget.textController.text,
|
||||
index: widget.index,
|
||||
),
|
||||
const SingleActivator(LogicalKeyboardKey.escape):
|
||||
@@ -386,39 +408,41 @@ class ChecklistItemControl extends StatelessWidget {
|
||||
return ChangeNotifierProvider.value(
|
||||
value: cellNotifer,
|
||||
child: Consumer<CellContainerNotifier>(
|
||||
builder: (buildContext, notifier, _) => GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
margin: const EdgeInsets.fromLTRB(8.0, 2.0, 8.0, 0),
|
||||
height: 12,
|
||||
child: AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 150),
|
||||
child: notifier.isHover
|
||||
? FlowyTooltip(
|
||||
message: LocaleKeys.grid_checklist_addNew.tr(),
|
||||
child: Row(
|
||||
children: [
|
||||
const Flexible(child: Center(child: Divider())),
|
||||
const HSpace(12.0),
|
||||
FilledButton(
|
||||
style: FilledButton.styleFrom(
|
||||
minimumSize: const Size.square(12),
|
||||
maximumSize: const Size.square(12),
|
||||
padding: EdgeInsets.zero,
|
||||
builder: (buildContext, notifier, _) => TextFieldTapRegion(
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
margin: const EdgeInsets.fromLTRB(8.0, 2.0, 8.0, 0),
|
||||
height: 12,
|
||||
child: AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 150),
|
||||
child: notifier.isHover
|
||||
? FlowyTooltip(
|
||||
message: LocaleKeys.grid_checklist_addNew.tr(),
|
||||
child: Row(
|
||||
children: [
|
||||
const Flexible(child: Center(child: Divider())),
|
||||
const HSpace(12.0),
|
||||
FilledButton(
|
||||
style: FilledButton.styleFrom(
|
||||
minimumSize: const Size.square(12),
|
||||
maximumSize: const Size.square(12),
|
||||
padding: EdgeInsets.zero,
|
||||
),
|
||||
onPressed: onTap,
|
||||
child: FlowySvg(
|
||||
FlowySvgs.add_s,
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
),
|
||||
onPressed: onTap,
|
||||
child: FlowySvg(
|
||||
FlowySvgs.add_s,
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
),
|
||||
const HSpace(12.0),
|
||||
const Flexible(child: Center(child: Divider())),
|
||||
],
|
||||
),
|
||||
)
|
||||
: const SizedBox.expand(),
|
||||
const HSpace(12.0),
|
||||
const Flexible(child: Center(child: Divider())),
|
||||
],
|
||||
),
|
||||
)
|
||||
: const SizedBox.expand(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
+1
-1
@@ -442,7 +442,7 @@ class _NewTaskItemState extends State<NewTaskItem> {
|
||||
? Theme.of(context).colorScheme.primaryContainer
|
||||
: Theme.of(context).disabledColor,
|
||||
fontColor: Theme.of(context).colorScheme.onPrimary,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
onPressed: isCreateButtonEnabled
|
||||
? () {
|
||||
context.read<ChecklistCellBloc>().add(
|
||||
|
||||
+1
-1
@@ -42,7 +42,7 @@ class ChecklistCellTextfield extends StatelessWidget {
|
||||
required this.focusNode,
|
||||
this.onChanged,
|
||||
this.contentPadding = const EdgeInsets.symmetric(
|
||||
vertical: 10,
|
||||
vertical: 8,
|
||||
horizontal: 2,
|
||||
),
|
||||
this.onSubmitted,
|
||||
|
||||
+107
-73
@@ -1,5 +1,7 @@
|
||||
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/bottom_sheet/bottom_sheet_media_upload.dart';
|
||||
import 'package:appflowy/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart';
|
||||
import 'package:appflowy/mobile/presentation/widgets/flowy_option_tile.dart';
|
||||
import 'package:appflowy/plugins/base/drag_handler.dart';
|
||||
@@ -10,7 +12,9 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/file/file_
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/common.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/multi_image_block_component/image_render.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/image_viewer/interactive_image_viewer.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/file_entities.pbenum.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/media_entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/row_entities.pb.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -25,10 +29,6 @@ class MobileMediaCellEditor extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.primaryContainer,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
constraints: const BoxConstraints.tightFor(height: 420),
|
||||
child: BlocProvider.value(
|
||||
value: context.read<MediaCellBloc>(),
|
||||
@@ -47,10 +47,28 @@ class MobileMediaCellEditor extends StatelessWidget {
|
||||
fontSize: 18,
|
||||
),
|
||||
),
|
||||
const Positioned(
|
||||
Positioned(
|
||||
top: 8,
|
||||
right: 18,
|
||||
child: FlowySvg(FlowySvgs.add_m, size: Size.square(28)),
|
||||
child: GestureDetector(
|
||||
onTap: () => showMobileBottomSheet(
|
||||
context,
|
||||
title: LocaleKeys.grid_media_addFileMobile.tr(),
|
||||
showHeader: true,
|
||||
showCloseButton: true,
|
||||
showDragHandle: true,
|
||||
builder: (dContext) => BlocProvider.value(
|
||||
value: context.read<MediaCellBloc>(),
|
||||
child: MobileMediaUploadSheetContent(
|
||||
dialogContext: dContext,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: const FlowySvg(
|
||||
FlowySvgs.add_m,
|
||||
size: Size.square(28),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -60,33 +78,6 @@ class MobileMediaCellEditor extends StatelessWidget {
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
// Padding(
|
||||
// padding: const EdgeInsets.all(8),
|
||||
// child: FlowyButton(
|
||||
// margin: const EdgeInsets.all(12),
|
||||
// onTap: () => showMobileBottomSheet(
|
||||
// context,
|
||||
// title: LocaleKeys.grid_media_addFileMobile.tr(),
|
||||
// showHeader: true,
|
||||
// showCloseButton: true,
|
||||
// showDragHandle: true,
|
||||
// builder: (dContext) =>
|
||||
// MobileMediaUploadSheetContent(
|
||||
// dialogContext: dContext,
|
||||
// ),
|
||||
// ),
|
||||
// text: const Row(
|
||||
// children: [
|
||||
// FlowySvg(
|
||||
// FlowySvgs.add_s,
|
||||
// size: Size.square(20),
|
||||
// ),
|
||||
// HSpace(8),
|
||||
// FlowyText('Add a file or image', fontSize: 15),
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
if (state.files.isNotEmpty) const Divider(height: .5),
|
||||
...state.files.map(
|
||||
(file) => Padding(
|
||||
@@ -115,27 +106,41 @@ class _FileItem extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.secondaryContainer,
|
||||
color: Theme.of(context).scaffoldBackgroundColor,
|
||||
),
|
||||
child: ListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
vertical: 12,
|
||||
horizontal: 16,
|
||||
),
|
||||
title: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
crossAxisAlignment: file.fileType == MediaFileTypePB.Image
|
||||
? CrossAxisAlignment.start
|
||||
: CrossAxisAlignment.center,
|
||||
children: [
|
||||
if (file.fileType != MediaFileTypePB.Image) ...[
|
||||
FlowySvg(file.fileType.icon, size: const Size.square(24)),
|
||||
const HSpace(12),
|
||||
Expanded(
|
||||
child: FlowyText(
|
||||
file.name,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
Flexible(
|
||||
child: GestureDetector(
|
||||
onTap: () => afLaunchUrlString(file.url),
|
||||
child: Row(
|
||||
children: [
|
||||
FlowySvg(file.fileType.icon, size: const Size.square(24)),
|
||||
const HSpace(12),
|
||||
Expanded(
|
||||
child: FlowyText(
|
||||
file.name,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
] else ...[
|
||||
Expanded(
|
||||
child: Container(
|
||||
margin: const EdgeInsets.symmetric(vertical: 12),
|
||||
alignment: Alignment.centerLeft,
|
||||
constraints: const BoxConstraints(maxHeight: 125),
|
||||
constraints: const BoxConstraints(maxHeight: 96),
|
||||
child: GestureDetector(
|
||||
onTap: () => openInteractiveViewer(context),
|
||||
child: ImageRender(
|
||||
@@ -153,13 +158,14 @@ class _FileItem extends StatelessWidget {
|
||||
),
|
||||
],
|
||||
FlowyIconButton(
|
||||
width: 40,
|
||||
width: 20,
|
||||
icon: const FlowySvg(
|
||||
FlowySvgs.three_dots_s,
|
||||
size: Size.square(20),
|
||||
),
|
||||
onPressed: () => showMobileBottomSheet(
|
||||
context,
|
||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||
showDragHandle: true,
|
||||
builder: (_) => BlocProvider.value(
|
||||
value: context.read<MediaCellBloc>(),
|
||||
@@ -189,10 +195,10 @@ class _EditFileSheet extends StatefulWidget {
|
||||
final MediaFilePB file;
|
||||
|
||||
@override
|
||||
State<_EditFileSheet> createState() => __EditFileSheetState();
|
||||
State<_EditFileSheet> createState() => _EditFileSheetState();
|
||||
}
|
||||
|
||||
class __EditFileSheetState extends State<_EditFileSheet> {
|
||||
class _EditFileSheetState extends State<_EditFileSheet> {
|
||||
late final controller = TextEditingController(text: widget.file.name);
|
||||
Loading? loader;
|
||||
|
||||
@@ -211,15 +217,9 @@ class __EditFileSheetState extends State<_EditFileSheet> {
|
||||
child: Column(
|
||||
children: [
|
||||
const VSpace(16),
|
||||
// _FileTextField(
|
||||
// file: file,
|
||||
// controller: controller,
|
||||
// onChanged: (name) =>
|
||||
// context.read<MediaCellBloc>().renameFile(file.id, name),
|
||||
// ),
|
||||
const VSpace(20),
|
||||
if (file.fileType == MediaFileTypePB.Image)
|
||||
if (file.fileType == MediaFileTypePB.Image) ...[
|
||||
FlowyOptionTile.text(
|
||||
showTopBorder: false,
|
||||
text: LocaleKeys.grid_media_expand.tr(),
|
||||
leftIcon: const FlowySvg(
|
||||
FlowySvgs.full_view_s,
|
||||
@@ -227,28 +227,62 @@ class __EditFileSheetState extends State<_EditFileSheet> {
|
||||
),
|
||||
onTap: () => openInteractiveViewer(context),
|
||||
),
|
||||
FlowyOptionTile.text(
|
||||
showTopBorder: false,
|
||||
text: LocaleKeys.grid_media_setAsCover.tr(),
|
||||
leftIcon: const FlowySvg(
|
||||
FlowySvgs.cover_s,
|
||||
size: Size.square(20),
|
||||
),
|
||||
onTap: () => context.read<MediaCellBloc>().add(
|
||||
MediaCellEvent.setCover(
|
||||
RowCoverPB(
|
||||
data: file.url,
|
||||
uploadType: file.uploadType,
|
||||
coverType: CoverTypePB.FileCover,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
FlowyOptionTile.text(
|
||||
text: file.fileType == MediaFileTypePB.Link
|
||||
? LocaleKeys.grid_media_expand.tr()
|
||||
: LocaleKeys.grid_media_download.tr(),
|
||||
leftIcon: FlowySvg(
|
||||
file.fileType == MediaFileTypePB.Link
|
||||
? FlowySvgs.m_link_m
|
||||
: FlowySvgs.import_s,
|
||||
size: const Size.square(20),
|
||||
),
|
||||
onTap: () async => downloadMediaFile(
|
||||
context,
|
||||
widget.file,
|
||||
userProfile: context.read<MediaCellBloc>().state.userProfile,
|
||||
onDownloadBegin: () {
|
||||
loader?.stop();
|
||||
loader = Loading(context);
|
||||
loader?.start();
|
||||
},
|
||||
onDownloadEnd: () => loader?.stop(),
|
||||
showTopBorder: file.fileType == MediaFileTypePB.Image,
|
||||
text: LocaleKeys.grid_media_openInBrowser.tr(),
|
||||
leftIcon: const FlowySvg(
|
||||
FlowySvgs.open_in_browser_s,
|
||||
size: Size.square(20),
|
||||
),
|
||||
onTap: () => afLaunchUrlString(file.url),
|
||||
),
|
||||
// TODO(Mathias): Rename interaction need design
|
||||
// FlowyOptionTile.text(
|
||||
// text: LocaleKeys.grid_media_rename.tr(),
|
||||
// leftIcon: const FlowySvg(
|
||||
// FlowySvgs.rename_s,
|
||||
// size: Size.square(20),
|
||||
// ),
|
||||
// onTap: () {},
|
||||
// ),
|
||||
if (widget.file.uploadType == FileUploadTypePB.CloudFile) ...[
|
||||
FlowyOptionTile.text(
|
||||
onTap: () async => downloadMediaFile(
|
||||
context,
|
||||
file,
|
||||
userProfile: context.read<MediaCellBloc>().state.userProfile,
|
||||
onDownloadBegin: () {
|
||||
loader?.stop();
|
||||
loader = Loading(context);
|
||||
loader?.start();
|
||||
},
|
||||
onDownloadEnd: () => loader?.stop(),
|
||||
),
|
||||
text: LocaleKeys.button_download.tr(),
|
||||
leftIcon: const FlowySvg(
|
||||
FlowySvgs.save_as_s,
|
||||
size: Size.square(20),
|
||||
),
|
||||
),
|
||||
],
|
||||
FlowyOptionTile.text(
|
||||
text: LocaleKeys.grid_media_delete.tr(),
|
||||
textColor: Theme.of(context).colorScheme.error,
|
||||
|
||||
+4
-2
@@ -30,8 +30,10 @@ class OptionButton extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _OptionButtonState extends State<OptionButton> {
|
||||
late final registerKey =
|
||||
_interceptorKey + widget.blockComponentContext.node.id;
|
||||
late final gestureInterceptor = SelectionGestureInterceptor(
|
||||
key: _interceptorKey,
|
||||
key: registerKey,
|
||||
canTap: (details) => !_isTapInBounds(details.globalPosition),
|
||||
);
|
||||
|
||||
@@ -52,7 +54,7 @@ class _OptionButtonState extends State<OptionButton> {
|
||||
@override
|
||||
void dispose() {
|
||||
widget.editorState.service.selectionService.unregisterGestureInterceptor(
|
||||
_interceptorKey,
|
||||
registerKey,
|
||||
);
|
||||
|
||||
super.dispose();
|
||||
|
||||
+1
@@ -640,6 +640,7 @@ SelectionMenuItem subPageSlashMenuItem = SelectionMenuItem.node(
|
||||
LocaleKeys.document_slashMenu_subPage_keyword5.tr(),
|
||||
LocaleKeys.document_slashMenu_subPage_keyword6.tr(),
|
||||
LocaleKeys.document_slashMenu_subPage_keyword7.tr(),
|
||||
LocaleKeys.document_slashMenu_subPage_keyword8.tr(),
|
||||
],
|
||||
updateSelection: (_, path, __, ___) =>
|
||||
Selection.collapsed(Position(path: path)),
|
||||
|
||||
+1
@@ -67,6 +67,7 @@ Future<void> _formatGreaterToToggleHeading(
|
||||
node.path,
|
||||
toggleListBlockNode(
|
||||
delta: delta,
|
||||
children: node.children.map((e) => e.copyWith()).toList(),
|
||||
),
|
||||
)
|
||||
..deleteNode(node);
|
||||
|
||||
@@ -37,7 +37,7 @@ class FileStorageService {
|
||||
final fileProgress = FileProgress.fromJsonString(event);
|
||||
if (fileProgress != null) {
|
||||
Log.debug(
|
||||
"Upload progress: file: ${fileProgress.fileUrl} ${fileProgress.progress}",
|
||||
"FileStorageService upload file: ${fileProgress.fileUrl} ${fileProgress.progress}",
|
||||
);
|
||||
final notifier = _notifierList[fileProgress.fileUrl];
|
||||
if (notifier != null) {
|
||||
|
||||
+8
-1
@@ -167,6 +167,10 @@ class SidebarPlanBloc extends Bloc<SidebarPlanEvent, SidebarPlanState> {
|
||||
),
|
||||
);
|
||||
},
|
||||
changedWorkspace: (workspaceId) {
|
||||
emit(state.copyWith(workspaceId: workspaceId));
|
||||
_checkWorkspaceUsage();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -200,6 +204,10 @@ class SidebarPlanEvent with _$SidebarPlanEvent {
|
||||
SidebarToastTierIndicator indicator,
|
||||
) = _UpdateTierIndicator;
|
||||
const factory SidebarPlanEvent.receiveError(FlowyError error) = _ReceiveError;
|
||||
|
||||
const factory SidebarPlanEvent.changedWorkspace({
|
||||
required String workspaceId,
|
||||
}) = _ChangedWorkspace;
|
||||
}
|
||||
|
||||
@freezed
|
||||
@@ -216,7 +224,6 @@ class SidebarPlanState with _$SidebarPlanState {
|
||||
|
||||
@freezed
|
||||
class SidebarToastTierIndicator with _$SidebarToastTierIndicator {
|
||||
// when start downloading the model
|
||||
const factory SidebarToastTierIndicator.storageLimitHit() = _StorageLimitHit;
|
||||
const factory SidebarToastTierIndicator.aiMaxiLimitHit() = _aiMaxLimitHit;
|
||||
const factory SidebarToastTierIndicator.loading() = _Loading;
|
||||
|
||||
+126
-117
@@ -75,131 +75,140 @@ class HomeSideBar extends StatelessWidget {
|
||||
// +-- Public Or Private Section: control the sections of the workspace
|
||||
// |
|
||||
// +-- Trash Section
|
||||
return BlocConsumer<UserWorkspaceBloc, UserWorkspaceState>(
|
||||
listenWhen: (previous, current) =>
|
||||
previous.currentWorkspace?.workspaceId !=
|
||||
current.currentWorkspace?.workspaceId,
|
||||
listener: (context, state) {
|
||||
if (FeatureFlag.search.isOn) {
|
||||
// Notify command palette that workspace has changed
|
||||
context.read<CommandPaletteBloc>().add(
|
||||
CommandPaletteEvent.workspaceChanged(
|
||||
workspaceId: state.currentWorkspace?.workspaceId,
|
||||
),
|
||||
);
|
||||
}
|
||||
return BlocProvider(
|
||||
create: (context) => SidebarPlanBloc()
|
||||
..add(SidebarPlanEvent.init(workspaceSetting.workspaceId, userProfile)),
|
||||
child: BlocConsumer<UserWorkspaceBloc, UserWorkspaceState>(
|
||||
listenWhen: (prev, curr) =>
|
||||
prev.currentWorkspace?.workspaceId !=
|
||||
curr.currentWorkspace?.workspaceId,
|
||||
listener: (context, state) {
|
||||
if (FeatureFlag.search.isOn) {
|
||||
// Notify command palette that workspace has changed
|
||||
context.read<CommandPaletteBloc>().add(
|
||||
CommandPaletteEvent.workspaceChanged(
|
||||
workspaceId: state.currentWorkspace?.workspaceId,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Re-initialize workspace-specific services
|
||||
getIt<CachedRecentService>().reset();
|
||||
},
|
||||
// Rebuild the whole sidebar when the current workspace changes
|
||||
buildWhen: (previous, current) =>
|
||||
previous.currentWorkspace?.workspaceId !=
|
||||
current.currentWorkspace?.workspaceId,
|
||||
builder: (context, state) {
|
||||
if (state.currentWorkspace == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
if (state.currentWorkspace != null) {
|
||||
context.read<SidebarPlanBloc>().add(
|
||||
SidebarPlanEvent.changedWorkspace(
|
||||
workspaceId: state.currentWorkspace!.workspaceId,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final workspaceId =
|
||||
state.currentWorkspace?.workspaceId ?? workspaceSetting.workspaceId;
|
||||
return MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider.value(value: getIt<ActionNavigationBloc>()),
|
||||
BlocProvider(
|
||||
create: (_) => SidebarSectionsBloc()
|
||||
..add(SidebarSectionsEvent.initial(userProfile, workspaceId)),
|
||||
),
|
||||
BlocProvider(
|
||||
create: (_) =>
|
||||
SpaceBloc(userProfile: userProfile, workspaceId: workspaceId)
|
||||
..add(const SpaceEvent.initial(openFirstPage: false)),
|
||||
),
|
||||
BlocProvider(
|
||||
create: (_) => SidebarPlanBloc()
|
||||
..add(SidebarPlanEvent.init(workspaceId, userProfile)),
|
||||
),
|
||||
],
|
||||
child: MultiBlocListener(
|
||||
listeners: [
|
||||
BlocListener<SidebarSectionsBloc, SidebarSectionsState>(
|
||||
listenWhen: (p, c) =>
|
||||
p.lastCreatedRootView?.id != c.lastCreatedRootView?.id,
|
||||
listener: (context, state) => context.read<TabsBloc>().add(
|
||||
TabsEvent.openPlugin(
|
||||
plugin: state.lastCreatedRootView!.plugin(),
|
||||
// Re-initialize workspace-specific services
|
||||
getIt<CachedRecentService>().reset();
|
||||
},
|
||||
// Rebuild the whole sidebar when the current workspace changes
|
||||
buildWhen: (previous, current) =>
|
||||
previous.currentWorkspace?.workspaceId !=
|
||||
current.currentWorkspace?.workspaceId,
|
||||
builder: (context, state) {
|
||||
if (state.currentWorkspace == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
final workspaceId = state.currentWorkspace?.workspaceId ??
|
||||
workspaceSetting.workspaceId;
|
||||
return MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider.value(value: getIt<ActionNavigationBloc>()),
|
||||
BlocProvider(
|
||||
create: (_) => SidebarSectionsBloc()
|
||||
..add(SidebarSectionsEvent.initial(userProfile, workspaceId)),
|
||||
),
|
||||
BlocProvider(
|
||||
create: (_) => SpaceBloc(
|
||||
userProfile: userProfile,
|
||||
workspaceId: workspaceId,
|
||||
)..add(const SpaceEvent.initial(openFirstPage: false)),
|
||||
),
|
||||
],
|
||||
child: MultiBlocListener(
|
||||
listeners: [
|
||||
BlocListener<SidebarSectionsBloc, SidebarSectionsState>(
|
||||
listenWhen: (p, c) =>
|
||||
p.lastCreatedRootView?.id != c.lastCreatedRootView?.id,
|
||||
listener: (context, state) => context.read<TabsBloc>().add(
|
||||
TabsEvent.openPlugin(
|
||||
plugin: state.lastCreatedRootView!.plugin(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
BlocListener<SpaceBloc, SpaceState>(
|
||||
listenWhen: (p, c) =>
|
||||
p.lastCreatedPage?.id != c.lastCreatedPage?.id ||
|
||||
p.isDuplicatingSpace != c.isDuplicatingSpace,
|
||||
listener: (context, state) {
|
||||
final page = state.lastCreatedPage;
|
||||
if (page == null || page.id.isEmpty) {
|
||||
// open the blank page
|
||||
context
|
||||
.read<TabsBloc>()
|
||||
.add(TabsEvent.openPlugin(plugin: BlankPagePlugin()));
|
||||
} else {
|
||||
context.read<TabsBloc>().add(
|
||||
TabsEvent.openPlugin(
|
||||
plugin: state.lastCreatedPage!.plugin(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (state.isDuplicatingSpace) {
|
||||
_duplicateSpaceLoading ??= Loading(context);
|
||||
_duplicateSpaceLoading?.start();
|
||||
} else if (_duplicateSpaceLoading != null) {
|
||||
_duplicateSpaceLoading?.stop();
|
||||
_duplicateSpaceLoading = null;
|
||||
}
|
||||
},
|
||||
),
|
||||
BlocListener<ActionNavigationBloc, ActionNavigationState>(
|
||||
listenWhen: (_, curr) => curr.action != null,
|
||||
listener: _onNotificationAction,
|
||||
),
|
||||
BlocListener<UserWorkspaceBloc, UserWorkspaceState>(
|
||||
listener: (context, state) {
|
||||
final actionType = state.actionResult?.actionType;
|
||||
|
||||
if (actionType == UserWorkspaceActionType.create ||
|
||||
actionType == UserWorkspaceActionType.delete ||
|
||||
actionType == UserWorkspaceActionType.open) {
|
||||
if (context.read<SpaceBloc>().state.spaces.isEmpty) {
|
||||
context.read<SidebarSectionsBloc>().add(
|
||||
SidebarSectionsEvent.reload(
|
||||
userProfile,
|
||||
state.currentWorkspace?.workspaceId ??
|
||||
workspaceSetting.workspaceId,
|
||||
),
|
||||
);
|
||||
),
|
||||
BlocListener<SpaceBloc, SpaceState>(
|
||||
listenWhen: (prev, curr) =>
|
||||
prev.lastCreatedPage?.id != curr.lastCreatedPage?.id ||
|
||||
prev.isDuplicatingSpace != curr.isDuplicatingSpace,
|
||||
listener: (context, state) {
|
||||
final page = state.lastCreatedPage;
|
||||
if (page == null || page.id.isEmpty) {
|
||||
// open the blank page
|
||||
context
|
||||
.read<TabsBloc>()
|
||||
.add(TabsEvent.openPlugin(plugin: BlankPagePlugin()));
|
||||
} else {
|
||||
context.read<SpaceBloc>().add(
|
||||
SpaceEvent.reset(
|
||||
userProfile,
|
||||
state.currentWorkspace?.workspaceId ??
|
||||
workspaceSetting.workspaceId,
|
||||
true,
|
||||
context.read<TabsBloc>().add(
|
||||
TabsEvent.openPlugin(
|
||||
plugin: state.lastCreatedPage!.plugin(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
context
|
||||
.read<FavoriteBloc>()
|
||||
.add(const FavoriteEvent.fetchFavorites());
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
child: _Sidebar(userProfile: userProfile),
|
||||
),
|
||||
);
|
||||
},
|
||||
if (state.isDuplicatingSpace) {
|
||||
_duplicateSpaceLoading ??= Loading(context);
|
||||
_duplicateSpaceLoading?.start();
|
||||
} else if (_duplicateSpaceLoading != null) {
|
||||
_duplicateSpaceLoading?.stop();
|
||||
_duplicateSpaceLoading = null;
|
||||
}
|
||||
},
|
||||
),
|
||||
BlocListener<ActionNavigationBloc, ActionNavigationState>(
|
||||
listenWhen: (_, curr) => curr.action != null,
|
||||
listener: _onNotificationAction,
|
||||
),
|
||||
BlocListener<UserWorkspaceBloc, UserWorkspaceState>(
|
||||
listener: (context, state) {
|
||||
final actionType = state.actionResult?.actionType;
|
||||
|
||||
if (actionType == UserWorkspaceActionType.create ||
|
||||
actionType == UserWorkspaceActionType.delete ||
|
||||
actionType == UserWorkspaceActionType.open) {
|
||||
if (context.read<SpaceBloc>().state.spaces.isEmpty) {
|
||||
context.read<SidebarSectionsBloc>().add(
|
||||
SidebarSectionsEvent.reload(
|
||||
userProfile,
|
||||
state.currentWorkspace?.workspaceId ??
|
||||
workspaceSetting.workspaceId,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
context.read<SpaceBloc>().add(
|
||||
SpaceEvent.reset(
|
||||
userProfile,
|
||||
state.currentWorkspace?.workspaceId ??
|
||||
workspaceSetting.workspaceId,
|
||||
true,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
context
|
||||
.read<FavoriteBloc>()
|
||||
.add(const FavoriteEvent.fetchFavorites());
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
child: _Sidebar(userProfile: userProfile),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+14
@@ -670,6 +670,13 @@ final _planLabels = [
|
||||
_PlanItem(
|
||||
label: LocaleKeys.settings_comparePlanDialog_planLabels_itemFileUpload.tr(),
|
||||
),
|
||||
_PlanItem(
|
||||
label:
|
||||
LocaleKeys.settings_comparePlanDialog_planLabels_customNamespace.tr(),
|
||||
tooltip: LocaleKeys
|
||||
.settings_comparePlanDialog_planLabels_customNamespaceTooltip
|
||||
.tr(),
|
||||
),
|
||||
];
|
||||
|
||||
class _CellItem {
|
||||
@@ -708,6 +715,9 @@ final List<_CellItem> _freeLabels = [
|
||||
_CellItem(
|
||||
label: LocaleKeys.settings_comparePlanDialog_freeLabels_itemFileUpload.tr(),
|
||||
),
|
||||
const _CellItem(
|
||||
label: '',
|
||||
),
|
||||
];
|
||||
|
||||
final List<_CellItem> _proLabels = [
|
||||
@@ -739,4 +749,8 @@ final List<_CellItem> _proLabels = [
|
||||
_CellItem(
|
||||
label: LocaleKeys.settings_comparePlanDialog_proLabels_itemFileUpload.tr(),
|
||||
),
|
||||
const _CellItem(
|
||||
label: '',
|
||||
icon: FlowySvgs.check_m,
|
||||
),
|
||||
];
|
||||
+24
-20
@@ -42,11 +42,7 @@ class DomainItem extends StatelessWidget {
|
||||
),
|
||||
// Homepage
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: SettingsPageSitesConstants.alignPadding,),
|
||||
child: _buildHomepage(context),
|
||||
),
|
||||
child: _buildHomepage(context),
|
||||
),
|
||||
// ... button
|
||||
DomainMoreAction(namespace: namespace),
|
||||
@@ -58,20 +54,23 @@ class DomainItem extends StatelessWidget {
|
||||
return Container(
|
||||
alignment: Alignment.centerLeft,
|
||||
padding: const EdgeInsets.only(right: 12.0),
|
||||
child: FlowyButton(
|
||||
useIntrinsicWidth: true,
|
||||
text: FlowyText(
|
||||
namespaceUrl,
|
||||
fontSize: 14.0,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
child: FlowyTooltip(
|
||||
message: '${LocaleKeys.shareAction_visitSite.tr()}\n$namespaceUrl',
|
||||
child: FlowyButton(
|
||||
useIntrinsicWidth: true,
|
||||
text: FlowyText(
|
||||
namespaceUrl,
|
||||
fontSize: 14.0,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
onTap: () {
|
||||
final namespaceUrl = ShareConstants.buildNamespaceUrl(
|
||||
nameSpace: namespace,
|
||||
withHttps: true,
|
||||
);
|
||||
afLaunchUrlString(namespaceUrl);
|
||||
},
|
||||
),
|
||||
onTap: () {
|
||||
final namespaceUrl = ShareConstants.buildNamespaceUrl(
|
||||
nameSpace: namespace,
|
||||
withHttps: true,
|
||||
);
|
||||
afLaunchUrlString(namespaceUrl);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -85,7 +84,12 @@ class DomainItem extends StatelessWidget {
|
||||
|
||||
final isFreePlan = plan == WorkspacePlanPB.FreePlan;
|
||||
if (isFreePlan) {
|
||||
return const _FreePlanUpgradeButton();
|
||||
return const Padding(
|
||||
padding: EdgeInsets.only(
|
||||
left: SettingsPageSitesConstants.alignPadding,
|
||||
),
|
||||
child: _FreePlanUpgradeButton(),
|
||||
);
|
||||
}
|
||||
|
||||
return const _HomePageButton();
|
||||
@@ -178,7 +182,7 @@ class _HomePageButton extends StatelessWidget {
|
||||
},
|
||||
text: const FlowySvg(
|
||||
FlowySvgs.close_m,
|
||||
size: Size.square(18.0),
|
||||
size: Size.square(19.0),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
+1
-1
@@ -271,7 +271,7 @@ abstract class AppFlowyDatePickerState<T extends AppFlowyDatePicker>
|
||||
|
||||
setState(() {
|
||||
isRange = value;
|
||||
dateTime = newDateTime;
|
||||
dateTime = focusedDateTime = newDateTime;
|
||||
if (value) {
|
||||
startDateTime = endDateTime = newDateTime;
|
||||
} else {
|
||||
|
||||
+2
-4
@@ -228,12 +228,11 @@ class DesktopAppFlowyDatePickerState
|
||||
|
||||
@override
|
||||
void onDateTimeInputSubmitted(DateTime value) {
|
||||
refreshStartTextFieldNotifier.refresh();
|
||||
if (isRange) {
|
||||
DateTime end = endDateTime ?? value;
|
||||
if (end.isBefore(value)) {
|
||||
(value, end) = (end, value);
|
||||
refreshEndTextFieldNotifier.refresh();
|
||||
refreshStartTextFieldNotifier.refresh();
|
||||
}
|
||||
|
||||
widget.onRangeSelected?.call(value, end);
|
||||
@@ -255,7 +254,6 @@ class DesktopAppFlowyDatePickerState
|
||||
|
||||
@override
|
||||
void onEndDateTimeInputSubmitted(DateTime value) {
|
||||
refreshEndTextFieldNotifier.refresh();
|
||||
if (isRange) {
|
||||
if (endDateTime == null) {
|
||||
value = combineDateTimes(value, widget.endDateTime);
|
||||
@@ -263,7 +261,7 @@ class DesktopAppFlowyDatePickerState
|
||||
DateTime start = startDateTime ?? value;
|
||||
if (value.isBefore(start)) {
|
||||
(start, value) = (value, start);
|
||||
refreshStartTextFieldNotifier.refresh();
|
||||
refreshEndTextFieldNotifier.refresh();
|
||||
}
|
||||
|
||||
widget.onRangeSelected?.call(start, value);
|
||||
|
||||
+11
-6
@@ -124,10 +124,14 @@ enum ReminderOption {
|
||||
required this.time,
|
||||
this.withoutTime = false,
|
||||
this.requiresNoTime = false,
|
||||
});
|
||||
}) : assert(!requiresNoTime || withoutTime);
|
||||
|
||||
final Duration time;
|
||||
|
||||
/// If true, don't consider the time component of the dateTime
|
||||
final bool withoutTime;
|
||||
|
||||
/// If true, [withoutTime] must be true as well. Will add time instead of subtract to get notification time.
|
||||
final bool requiresNoTime;
|
||||
|
||||
bool get timeExempt =>
|
||||
@@ -190,10 +194,11 @@ enum ReminderOption {
|
||||
_ => ReminderOption.custom,
|
||||
};
|
||||
|
||||
DateTime getNotificationDateTime(DateTime date) => switch (withoutTime) {
|
||||
true => requiresNoTime
|
||||
DateTime getNotificationDateTime(DateTime date) {
|
||||
return withoutTime
|
||||
? requiresNoTime
|
||||
? date.withoutTime.add(time)
|
||||
: date.withoutTime.subtract(time),
|
||||
_ => date.subtract(time),
|
||||
};
|
||||
: date.withoutTime.subtract(time)
|
||||
: date.subtract(time);
|
||||
}
|
||||
}
|
||||
@@ -61,8 +61,8 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "."
|
||||
ref: "2903792"
|
||||
resolved-ref: "2903792fa319e1b4077164eeb684f6e8d1c63e27"
|
||||
ref: "76daa96"
|
||||
resolved-ref: "76daa96af51f0ad4e881c10426a91780977544e5"
|
||||
url: "https://github.com/AppFlowy-IO/appflowy-editor.git"
|
||||
source: git
|
||||
version: "4.0.0"
|
||||
|
||||
@@ -172,7 +172,7 @@ dependency_overrides:
|
||||
appflowy_editor:
|
||||
git:
|
||||
url: https://github.com/AppFlowy-IO/appflowy-editor.git
|
||||
ref: "2903792"
|
||||
ref: "76daa96"
|
||||
|
||||
appflowy_editor_plugins:
|
||||
git:
|
||||
|
||||
@@ -2,7 +2,9 @@ import 'package:appflowy/plugins/database/application/cell/bloc/date_cell_editor
|
||||
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
|
||||
import 'package:appflowy/plugins/database/domain/field_service.dart';
|
||||
import 'package:appflowy/user/application/reminder/reminder_bloc.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/reminder_selector.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||
import 'package:fixnum/fixnum.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:time/time.dart';
|
||||
|
||||
@@ -249,5 +251,141 @@ void main() {
|
||||
TimeFormatPB.TwelveHour,
|
||||
);
|
||||
});
|
||||
|
||||
test('set reminder option', () async {
|
||||
final reminderBloc = ReminderBloc();
|
||||
final bloc = DateCellEditorBloc(
|
||||
cellController: cellController,
|
||||
reminderBloc: reminderBloc,
|
||||
);
|
||||
await gridResponseFuture();
|
||||
|
||||
expect(reminderBloc.state.reminders.length, 0);
|
||||
|
||||
final now = DateTime.now();
|
||||
final threeDaysFromToday = DateTime(now.year, now.month, now.day + 3);
|
||||
final fourDaysFromToday = DateTime(now.year, now.month, now.day + 4);
|
||||
final fiveDaysFromToday = DateTime(now.year, now.month, now.day + 5);
|
||||
|
||||
bloc.add(DateCellEditorEvent.updateDateTime(threeDaysFromToday));
|
||||
await gridResponseFuture();
|
||||
|
||||
bloc.add(
|
||||
const DateCellEditorEvent.setReminderOption(
|
||||
ReminderOption.onDayOfEvent,
|
||||
),
|
||||
);
|
||||
await gridResponseFuture();
|
||||
|
||||
expect(reminderBloc.state.reminders.length, 1);
|
||||
expect(
|
||||
reminderBloc.state.reminders.first.scheduledAt,
|
||||
Int64(
|
||||
threeDaysFromToday
|
||||
.add(const Duration(hours: 9))
|
||||
.millisecondsSinceEpoch ~/
|
||||
1000,
|
||||
),
|
||||
);
|
||||
|
||||
reminderBloc.add(const ReminderEvent.refresh());
|
||||
await gridResponseFuture();
|
||||
|
||||
expect(reminderBloc.state.reminders.length, 1);
|
||||
expect(
|
||||
reminderBloc.state.reminders.first.scheduledAt,
|
||||
Int64(
|
||||
threeDaysFromToday
|
||||
.add(const Duration(hours: 9))
|
||||
.millisecondsSinceEpoch ~/
|
||||
1000,
|
||||
),
|
||||
);
|
||||
|
||||
bloc.add(DateCellEditorEvent.updateDateTime(fourDaysFromToday));
|
||||
await gridResponseFuture();
|
||||
expect(reminderBloc.state.reminders.length, 1);
|
||||
expect(
|
||||
reminderBloc.state.reminders.first.scheduledAt,
|
||||
Int64(
|
||||
fourDaysFromToday
|
||||
.add(const Duration(hours: 9))
|
||||
.millisecondsSinceEpoch ~/
|
||||
1000,
|
||||
),
|
||||
);
|
||||
|
||||
bloc.add(DateCellEditorEvent.updateDateTime(fiveDaysFromToday));
|
||||
await gridResponseFuture();
|
||||
reminderBloc.add(const ReminderEvent.refresh());
|
||||
await gridResponseFuture();
|
||||
expect(reminderBloc.state.reminders.length, 1);
|
||||
expect(
|
||||
reminderBloc.state.reminders.first.scheduledAt,
|
||||
Int64(
|
||||
fiveDaysFromToday
|
||||
.add(const Duration(hours: 9))
|
||||
.millisecondsSinceEpoch ~/
|
||||
1000,
|
||||
),
|
||||
);
|
||||
|
||||
bloc.add(
|
||||
const DateCellEditorEvent.setReminderOption(
|
||||
ReminderOption.twoDaysBefore,
|
||||
),
|
||||
);
|
||||
await gridResponseFuture();
|
||||
expect(reminderBloc.state.reminders.length, 1);
|
||||
expect(
|
||||
reminderBloc.state.reminders.first.scheduledAt,
|
||||
Int64(
|
||||
threeDaysFromToday
|
||||
.add(const Duration(hours: 9))
|
||||
.millisecondsSinceEpoch ~/
|
||||
1000,
|
||||
),
|
||||
);
|
||||
|
||||
bloc.add(
|
||||
const DateCellEditorEvent.setReminderOption(ReminderOption.none),
|
||||
);
|
||||
await gridResponseFuture();
|
||||
expect(reminderBloc.state.reminders.length, 0);
|
||||
reminderBloc.add(const ReminderEvent.refresh());
|
||||
await gridResponseFuture();
|
||||
expect(reminderBloc.state.reminders.length, 0);
|
||||
});
|
||||
|
||||
test('set reminder option from empty', () async {
|
||||
final reminderBloc = ReminderBloc();
|
||||
final bloc = DateCellEditorBloc(
|
||||
cellController: cellController,
|
||||
reminderBloc: reminderBloc,
|
||||
);
|
||||
await gridResponseFuture();
|
||||
|
||||
final now = DateTime.now();
|
||||
final today = DateTime(now.year, now.month, now.day);
|
||||
bloc.add(
|
||||
const DateCellEditorEvent.setReminderOption(
|
||||
ReminderOption.onDayOfEvent,
|
||||
),
|
||||
);
|
||||
await gridResponseFuture();
|
||||
|
||||
expect(bloc.state.dateTime, today);
|
||||
expect(reminderBloc.state.reminders.length, 1);
|
||||
expect(
|
||||
reminderBloc.state.reminders.first.scheduledAt,
|
||||
Int64(
|
||||
today.add(const Duration(hours: 9)).millisecondsSinceEpoch ~/ 1000,
|
||||
),
|
||||
);
|
||||
|
||||
bloc.add(const DateCellEditorEvent.clearDate());
|
||||
await gridResponseFuture();
|
||||
expect(reminderBloc.state.reminders.length, 0);
|
||||
});
|
||||
});
|
||||
}
|
||||
+35
@@ -49,5 +49,40 @@ void main() {
|
||||
|
||||
editorState.dispose();
|
||||
});
|
||||
|
||||
testWidgets('convert block contains children to toggle list',
|
||||
(tester) async {
|
||||
const paragraph1 = '>paragraph 1';
|
||||
const paragraph1_1 = 'paragraph 1.1';
|
||||
const paragraph1_2 = 'paragraph 1.2';
|
||||
|
||||
final document = createDocument([
|
||||
paragraphNode(
|
||||
text: paragraph1,
|
||||
children: [
|
||||
paragraphNode(text: paragraph1_1),
|
||||
paragraphNode(text: paragraph1_2),
|
||||
],
|
||||
),
|
||||
]);
|
||||
|
||||
final editorState = EditorState(document: document);
|
||||
editorState.selection = Selection.collapsed(
|
||||
Position(path: [0], offset: 1),
|
||||
);
|
||||
|
||||
final result = await formatGreaterToToggleList.execute(editorState);
|
||||
expect(result, true);
|
||||
|
||||
expect(editorState.document.root.children.length, 1);
|
||||
final node = editorState.document.root.children[0];
|
||||
expect(node.type, ToggleListBlockKeys.type);
|
||||
expect(node.delta!.toPlainText(), 'paragraph 1');
|
||||
expect(node.children.length, 2);
|
||||
expect(node.children[0].delta!.toPlainText(), paragraph1_1);
|
||||
expect(node.children[1].delta!.toPlainText(), paragraph1_2);
|
||||
|
||||
editorState.dispose();
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -17,7 +17,7 @@ bool FlutterWindow::OnCreate() {
|
||||
RECT frame = GetClientArea();
|
||||
|
||||
// The size here must match the window dimensions to avoid unnecessary surface
|
||||
// creation / destruction in the startup path.
|
||||
// creation / destruction in the startup path.
|
||||
flutter_controller_ = std::make_unique<flutter::FlutterViewController>(
|
||||
frame.right - frame.left, frame.bottom - frame.top, project_);
|
||||
// Ensure that basic setup of the controller was successful.
|
||||
@@ -26,6 +26,16 @@ bool FlutterWindow::OnCreate() {
|
||||
}
|
||||
RegisterPlugins(flutter_controller_->engine());
|
||||
SetChildContent(flutter_controller_->view()->GetNativeWindow());
|
||||
|
||||
flutter_controller_->engine()->SetNextFrameCallback([&]() {
|
||||
this->Show();
|
||||
});
|
||||
|
||||
// Flutter can complete the first frame before the "show window" callback is
|
||||
// registered. The following call ensures a frame is pending to ensure the
|
||||
// window is shown. It is a no-op if the first frame hasn't completed yet.
|
||||
flutter_controller_->ForceRedraw();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -47,9 +47,12 @@ int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,
|
||||
FlutterWindow window(project);
|
||||
Win32Window::Point origin(10, 10);
|
||||
Win32Window::Size size(1280, 720);
|
||||
if (!window.CreateAndShow(L"AppFlowy", origin, size)) {
|
||||
|
||||
if (!window.Create(L"AppFlowy", origin, size)) {
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
window.Show();
|
||||
window.SetQuitOnClose(true);
|
||||
|
||||
::MSG msg;
|
||||
|
||||
@@ -1,60 +1,70 @@
|
||||
#include "win32_window.h"
|
||||
|
||||
#include <dwmapi.h>
|
||||
#include <flutter_windows.h>
|
||||
|
||||
#include "resource.h"
|
||||
|
||||
#include "app_links/app_links_plugin_c_api.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
namespace {
|
||||
|
||||
constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW";
|
||||
/// Window attribute that enables dark mode window decorations.
|
||||
///
|
||||
/// Redefined in case the developer's machine has a Windows SDK older than
|
||||
/// version 10.0.22000.0.
|
||||
/// See: https://docs.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute
|
||||
#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE
|
||||
#define DWMWA_USE_IMMERSIVE_DARK_MODE 20
|
||||
#endif
|
||||
|
||||
// The number of Win32Window objects that currently exist.
|
||||
static int g_active_window_count = 0;
|
||||
constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW";
|
||||
|
||||
using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd);
|
||||
/// Registry key for app theme preference.
|
||||
///
|
||||
/// A value of 0 indicates apps should use dark mode. A non-zero or missing
|
||||
/// value indicates apps should use light mode.
|
||||
constexpr const wchar_t kGetPreferredBrightnessRegKey[] =
|
||||
L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize";
|
||||
constexpr const wchar_t kGetPreferredBrightnessRegValue[] = L"AppsUseLightTheme";
|
||||
|
||||
// Scale helper to convert logical scaler values to physical using passed in
|
||||
// scale factor
|
||||
int Scale(int source, double scale_factor)
|
||||
{
|
||||
return static_cast<int>(source * scale_factor);
|
||||
// The number of Win32Window objects that currently exist.
|
||||
static int g_active_window_count = 0;
|
||||
|
||||
using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd);
|
||||
|
||||
// Scale helper to convert logical scaler values to physical using passed in
|
||||
// scale factor
|
||||
int Scale(int source, double scale_factor) {
|
||||
return static_cast<int>(source * scale_factor);
|
||||
}
|
||||
|
||||
// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module.
|
||||
// This API is only needed for PerMonitor V1 awareness mode.
|
||||
void EnableFullDpiSupportIfAvailable(HWND hwnd) {
|
||||
HMODULE user32_module = LoadLibraryA("User32.dll");
|
||||
if (!user32_module) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module.
|
||||
// This API is only needed for PerMonitor V1 awareness mode.
|
||||
void EnableFullDpiSupportIfAvailable(HWND hwnd)
|
||||
{
|
||||
HMODULE user32_module = LoadLibraryA("User32.dll");
|
||||
if (!user32_module)
|
||||
{
|
||||
return;
|
||||
}
|
||||
auto enable_non_client_dpi_scaling =
|
||||
reinterpret_cast<EnableNonClientDpiScaling *>(
|
||||
GetProcAddress(user32_module, "EnableNonClientDpiScaling"));
|
||||
if (enable_non_client_dpi_scaling != nullptr)
|
||||
{
|
||||
enable_non_client_dpi_scaling(hwnd);
|
||||
FreeLibrary(user32_module);
|
||||
}
|
||||
auto enable_non_client_dpi_scaling =
|
||||
reinterpret_cast<EnableNonClientDpiScaling*>(
|
||||
GetProcAddress(user32_module, "EnableNonClientDpiScaling"));
|
||||
if (enable_non_client_dpi_scaling != nullptr) {
|
||||
enable_non_client_dpi_scaling(hwnd);
|
||||
}
|
||||
FreeLibrary(user32_module);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace
|
||||
|
||||
// Manages the Win32Window's window class registration.
|
||||
class WindowClassRegistrar
|
||||
{
|
||||
public:
|
||||
class WindowClassRegistrar {
|
||||
public:
|
||||
~WindowClassRegistrar() = default;
|
||||
|
||||
// Returns the singleton registar instance.
|
||||
static WindowClassRegistrar *GetInstance()
|
||||
{
|
||||
if (!instance_)
|
||||
{
|
||||
// Returns the singleton registrar instance.
|
||||
static WindowClassRegistrar* GetInstance() {
|
||||
if (!instance_) {
|
||||
instance_ = new WindowClassRegistrar();
|
||||
}
|
||||
return instance_;
|
||||
@@ -62,26 +72,24 @@ public:
|
||||
|
||||
// Returns the name of the window class, registering the class if it hasn't
|
||||
// previously been registered.
|
||||
const wchar_t *GetWindowClass();
|
||||
const wchar_t* GetWindowClass();
|
||||
|
||||
// Unregisters the window class. Should only be called if there are no
|
||||
// instances of the window.
|
||||
void UnregisterWindowClass();
|
||||
|
||||
private:
|
||||
private:
|
||||
WindowClassRegistrar() = default;
|
||||
|
||||
static WindowClassRegistrar *instance_;
|
||||
static WindowClassRegistrar* instance_;
|
||||
|
||||
bool class_registered_ = false;
|
||||
};
|
||||
|
||||
WindowClassRegistrar *WindowClassRegistrar::instance_ = nullptr;
|
||||
WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr;
|
||||
|
||||
const wchar_t *WindowClassRegistrar::GetWindowClass()
|
||||
{
|
||||
if (!class_registered_)
|
||||
{
|
||||
const wchar_t* WindowClassRegistrar::GetWindowClass() {
|
||||
if (!class_registered_) {
|
||||
WNDCLASS window_class{};
|
||||
window_class.hCursor = LoadCursor(nullptr, IDC_ARROW);
|
||||
window_class.lpszClassName = kWindowClassName;
|
||||
@@ -100,35 +108,31 @@ const wchar_t *WindowClassRegistrar::GetWindowClass()
|
||||
return kWindowClassName;
|
||||
}
|
||||
|
||||
void WindowClassRegistrar::UnregisterWindowClass()
|
||||
{
|
||||
void WindowClassRegistrar::UnregisterWindowClass() {
|
||||
UnregisterClass(kWindowClassName, nullptr);
|
||||
class_registered_ = false;
|
||||
}
|
||||
|
||||
Win32Window::Win32Window()
|
||||
{
|
||||
Win32Window::Win32Window() {
|
||||
++g_active_window_count;
|
||||
}
|
||||
|
||||
Win32Window::~Win32Window()
|
||||
{
|
||||
Win32Window::~Win32Window() {
|
||||
--g_active_window_count;
|
||||
Destroy();
|
||||
}
|
||||
|
||||
bool Win32Window::CreateAndShow(const std::wstring &title,
|
||||
const Point &origin,
|
||||
const Size &size)
|
||||
{
|
||||
bool Win32Window::Create(const std::wstring& title,
|
||||
const Point& origin,
|
||||
const Size& size) {
|
||||
Destroy();
|
||||
|
||||
if (SendAppLinkToInstance(title))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Destroy();
|
||||
|
||||
const wchar_t *window_class =
|
||||
const wchar_t* window_class =
|
||||
WindowClassRegistrar::GetInstance()->GetWindowClass();
|
||||
|
||||
const POINT target_point = {static_cast<LONG>(origin.x),
|
||||
@@ -138,19 +142,158 @@ bool Win32Window::CreateAndShow(const std::wstring &title,
|
||||
double scale_factor = dpi / 96.0;
|
||||
|
||||
HWND window = CreateWindow(
|
||||
window_class, title.c_str(), WS_OVERLAPPEDWINDOW | WS_VISIBLE,
|
||||
window_class, title.c_str(), WS_OVERLAPPEDWINDOW,
|
||||
Scale(origin.x, scale_factor), Scale(origin.y, scale_factor),
|
||||
Scale(size.width, scale_factor), Scale(size.height, scale_factor),
|
||||
nullptr, nullptr, GetModuleHandle(nullptr), this);
|
||||
|
||||
if (!window)
|
||||
{
|
||||
if (!window) {
|
||||
return false;
|
||||
}
|
||||
|
||||
UpdateTheme(window);
|
||||
|
||||
return OnCreate();
|
||||
}
|
||||
|
||||
bool Win32Window::Show() {
|
||||
return ShowWindow(window_handle_, SW_SHOWNORMAL);
|
||||
}
|
||||
|
||||
// static
|
||||
LRESULT CALLBACK Win32Window::WndProc(HWND const window,
|
||||
UINT const message,
|
||||
WPARAM const wparam,
|
||||
LPARAM const lparam) noexcept {
|
||||
if (message == WM_NCCREATE) {
|
||||
auto window_struct = reinterpret_cast<CREATESTRUCT*>(lparam);
|
||||
SetWindowLongPtr(window, GWLP_USERDATA,
|
||||
reinterpret_cast<LONG_PTR>(window_struct->lpCreateParams));
|
||||
|
||||
auto that = static_cast<Win32Window*>(window_struct->lpCreateParams);
|
||||
EnableFullDpiSupportIfAvailable(window);
|
||||
that->window_handle_ = window;
|
||||
} else if (Win32Window* that = GetThisFromHandle(window)) {
|
||||
return that->MessageHandler(window, message, wparam, lparam);
|
||||
}
|
||||
|
||||
return DefWindowProc(window, message, wparam, lparam);
|
||||
}
|
||||
|
||||
LRESULT
|
||||
Win32Window::MessageHandler(HWND hwnd,
|
||||
UINT const message,
|
||||
WPARAM const wparam,
|
||||
LPARAM const lparam) noexcept {
|
||||
switch (message) {
|
||||
case WM_DESTROY:
|
||||
window_handle_ = nullptr;
|
||||
Destroy();
|
||||
if (quit_on_close_) {
|
||||
PostQuitMessage(0);
|
||||
}
|
||||
return 0;
|
||||
|
||||
case WM_DPICHANGED: {
|
||||
auto newRectSize = reinterpret_cast<RECT*>(lparam);
|
||||
LONG newWidth = newRectSize->right - newRectSize->left;
|
||||
LONG newHeight = newRectSize->bottom - newRectSize->top;
|
||||
|
||||
SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth,
|
||||
newHeight, SWP_NOZORDER | SWP_NOACTIVATE);
|
||||
|
||||
return 0;
|
||||
}
|
||||
case WM_SIZE: {
|
||||
RECT rect = GetClientArea();
|
||||
if (child_content_ != nullptr) {
|
||||
// Size and position the child window.
|
||||
MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left,
|
||||
rect.bottom - rect.top, TRUE);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
case WM_ACTIVATE:
|
||||
if (child_content_ != nullptr) {
|
||||
SetFocus(child_content_);
|
||||
}
|
||||
return 0;
|
||||
|
||||
case WM_DWMCOLORIZATIONCOLORCHANGED:
|
||||
UpdateTheme(hwnd);
|
||||
return 0;
|
||||
}
|
||||
|
||||
return DefWindowProc(window_handle_, message, wparam, lparam);
|
||||
}
|
||||
|
||||
void Win32Window::Destroy() {
|
||||
OnDestroy();
|
||||
|
||||
if (window_handle_) {
|
||||
DestroyWindow(window_handle_);
|
||||
window_handle_ = nullptr;
|
||||
}
|
||||
if (g_active_window_count == 0) {
|
||||
WindowClassRegistrar::GetInstance()->UnregisterWindowClass();
|
||||
}
|
||||
}
|
||||
|
||||
Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept {
|
||||
return reinterpret_cast<Win32Window*>(
|
||||
GetWindowLongPtr(window, GWLP_USERDATA));
|
||||
}
|
||||
|
||||
void Win32Window::SetChildContent(HWND content) {
|
||||
child_content_ = content;
|
||||
SetParent(content, window_handle_);
|
||||
RECT frame = GetClientArea();
|
||||
|
||||
MoveWindow(content, frame.left, frame.top, frame.right - frame.left,
|
||||
frame.bottom - frame.top, true);
|
||||
|
||||
SetFocus(child_content_);
|
||||
}
|
||||
|
||||
RECT Win32Window::GetClientArea() {
|
||||
RECT frame;
|
||||
GetClientRect(window_handle_, &frame);
|
||||
return frame;
|
||||
}
|
||||
|
||||
HWND Win32Window::GetHandle() {
|
||||
return window_handle_;
|
||||
}
|
||||
|
||||
void Win32Window::SetQuitOnClose(bool quit_on_close) {
|
||||
quit_on_close_ = quit_on_close;
|
||||
}
|
||||
|
||||
bool Win32Window::OnCreate() {
|
||||
// No-op; provided for subclasses.
|
||||
return true;
|
||||
}
|
||||
|
||||
void Win32Window::OnDestroy() {
|
||||
// No-op; provided for subclasses.
|
||||
}
|
||||
|
||||
void Win32Window::UpdateTheme(HWND const window) {
|
||||
DWORD light_mode;
|
||||
DWORD light_mode_size = sizeof(light_mode);
|
||||
LSTATUS result = RegGetValue(HKEY_CURRENT_USER, kGetPreferredBrightnessRegKey,
|
||||
kGetPreferredBrightnessRegValue,
|
||||
RRF_RT_REG_DWORD, nullptr, &light_mode,
|
||||
&light_mode_size);
|
||||
|
||||
if (result == ERROR_SUCCESS) {
|
||||
BOOL enable_dark_mode = light_mode == 0;
|
||||
DwmSetWindowAttribute(window, DWMWA_USE_IMMERSIVE_DARK_MODE,
|
||||
&enable_dark_mode, sizeof(enable_dark_mode));
|
||||
}
|
||||
}
|
||||
|
||||
bool Win32Window::SendAppLinkToInstance(const std::wstring &title)
|
||||
{
|
||||
// Find our exact window
|
||||
@@ -186,140 +329,4 @@ bool Win32Window::SendAppLinkToInstance(const std::wstring &title)
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// static
|
||||
LRESULT CALLBACK Win32Window::WndProc(HWND const window,
|
||||
UINT const message,
|
||||
WPARAM const wparam,
|
||||
LPARAM const lparam) noexcept
|
||||
{
|
||||
if (message == WM_NCCREATE)
|
||||
{
|
||||
auto window_struct = reinterpret_cast<CREATESTRUCT *>(lparam);
|
||||
SetWindowLongPtr(window, GWLP_USERDATA,
|
||||
reinterpret_cast<LONG_PTR>(window_struct->lpCreateParams));
|
||||
|
||||
auto that = static_cast<Win32Window *>(window_struct->lpCreateParams);
|
||||
EnableFullDpiSupportIfAvailable(window);
|
||||
that->window_handle_ = window;
|
||||
}
|
||||
else if (Win32Window *that = GetThisFromHandle(window))
|
||||
{
|
||||
return that->MessageHandler(window, message, wparam, lparam);
|
||||
}
|
||||
|
||||
return DefWindowProc(window, message, wparam, lparam);
|
||||
}
|
||||
|
||||
LRESULT
|
||||
Win32Window::MessageHandler(HWND hwnd,
|
||||
UINT const message,
|
||||
WPARAM const wparam,
|
||||
LPARAM const lparam) noexcept
|
||||
{
|
||||
switch (message)
|
||||
{
|
||||
case WM_DESTROY:
|
||||
window_handle_ = nullptr;
|
||||
Destroy();
|
||||
if (quit_on_close_)
|
||||
{
|
||||
PostQuitMessage(0);
|
||||
}
|
||||
return 0;
|
||||
|
||||
case WM_DPICHANGED:
|
||||
{
|
||||
auto newRectSize = reinterpret_cast<RECT *>(lparam);
|
||||
LONG newWidth = newRectSize->right - newRectSize->left;
|
||||
LONG newHeight = newRectSize->bottom - newRectSize->top;
|
||||
|
||||
SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth,
|
||||
newHeight, SWP_NOZORDER | SWP_NOACTIVATE);
|
||||
|
||||
return 0;
|
||||
}
|
||||
case WM_SIZE:
|
||||
{
|
||||
RECT rect = GetClientArea();
|
||||
if (child_content_ != nullptr)
|
||||
{
|
||||
// Size and position the child window.
|
||||
MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left,
|
||||
rect.bottom - rect.top, TRUE);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
case WM_ACTIVATE:
|
||||
if (child_content_ != nullptr)
|
||||
{
|
||||
SetFocus(child_content_);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
return DefWindowProc(window_handle_, message, wparam, lparam);
|
||||
}
|
||||
|
||||
void Win32Window::Destroy()
|
||||
{
|
||||
OnDestroy();
|
||||
|
||||
if (window_handle_)
|
||||
{
|
||||
DestroyWindow(window_handle_);
|
||||
window_handle_ = nullptr;
|
||||
}
|
||||
if (g_active_window_count == 0)
|
||||
{
|
||||
WindowClassRegistrar::GetInstance()->UnregisterWindowClass();
|
||||
}
|
||||
}
|
||||
|
||||
Win32Window *Win32Window::GetThisFromHandle(HWND const window) noexcept
|
||||
{
|
||||
return reinterpret_cast<Win32Window *>(
|
||||
GetWindowLongPtr(window, GWLP_USERDATA));
|
||||
}
|
||||
|
||||
void Win32Window::SetChildContent(HWND content)
|
||||
{
|
||||
child_content_ = content;
|
||||
SetParent(content, window_handle_);
|
||||
RECT frame = GetClientArea();
|
||||
|
||||
MoveWindow(content, frame.left, frame.top, frame.right - frame.left,
|
||||
frame.bottom - frame.top, true);
|
||||
|
||||
SetFocus(child_content_);
|
||||
}
|
||||
|
||||
RECT Win32Window::GetClientArea()
|
||||
{
|
||||
RECT frame;
|
||||
GetClientRect(window_handle_, &frame);
|
||||
return frame;
|
||||
}
|
||||
|
||||
HWND Win32Window::GetHandle()
|
||||
{
|
||||
return window_handle_;
|
||||
}
|
||||
|
||||
void Win32Window::SetQuitOnClose(bool quit_on_close)
|
||||
{
|
||||
quit_on_close_ = quit_on_close;
|
||||
}
|
||||
|
||||
bool Win32Window::OnCreate()
|
||||
{
|
||||
// No-op; provided for subclasses.
|
||||
return true;
|
||||
}
|
||||
|
||||
void Win32Window::OnDestroy()
|
||||
{
|
||||
// No-op; provided for subclasses.
|
||||
}
|
||||
}
|
||||
@@ -10,18 +10,15 @@
|
||||
// A class abstraction for a high DPI-aware Win32 Window. Intended to be
|
||||
// inherited from by classes that wish to specialize with custom
|
||||
// rendering and input handling
|
||||
class Win32Window
|
||||
{
|
||||
public:
|
||||
struct Point
|
||||
{
|
||||
class Win32Window {
|
||||
public:
|
||||
struct Point {
|
||||
unsigned int x;
|
||||
unsigned int y;
|
||||
Point(unsigned int x, unsigned int y) : x(x), y(y) {}
|
||||
};
|
||||
|
||||
struct Size
|
||||
{
|
||||
struct Size {
|
||||
unsigned int width;
|
||||
unsigned int height;
|
||||
Size(unsigned int width, unsigned int height)
|
||||
@@ -31,19 +28,16 @@ public:
|
||||
Win32Window();
|
||||
virtual ~Win32Window();
|
||||
|
||||
// Creates and shows a win32 window with |title| and position and size using
|
||||
// Creates a win32 window with |title| that is positioned and sized using
|
||||
// |origin| and |size|. New windows are created on the default monitor. Window
|
||||
// sizes are specified to the OS in physical pixels, hence to ensure a
|
||||
// consistent size to will treat the width height passed in to this function
|
||||
// as logical pixels and scale to appropriate for the default monitor. Returns
|
||||
// true if the window was created successfully.
|
||||
bool CreateAndShow(const std::wstring &title,
|
||||
const Point &origin,
|
||||
const Size &size);
|
||||
// consistent size this function will scale the inputted width and height as
|
||||
// as appropriate for the default monitor. The window is invisible until
|
||||
// |Show| is called. Returns true if the window was created successfully.
|
||||
bool Create(const std::wstring& title, const Point& origin, const Size& size);
|
||||
|
||||
// Dispatches link if any.
|
||||
// This method enables our app to be with a single instance too.
|
||||
bool SendAppLinkToInstance(const std::wstring &title);
|
||||
// Show the current window. Returns true if the window was successfully shown.
|
||||
bool Show();
|
||||
|
||||
// Release OS resources associated with window.
|
||||
void Destroy();
|
||||
@@ -61,7 +55,11 @@ public:
|
||||
// Return a RECT representing the bounds of the current client area.
|
||||
RECT GetClientArea();
|
||||
|
||||
protected:
|
||||
// Dispatches link if any.
|
||||
// This method enables our app to be with a single instance too.
|
||||
bool SendAppLinkToInstance(const std::wstring &title);
|
||||
|
||||
protected:
|
||||
// Processes and route salient window messages for mouse handling,
|
||||
// size change and DPI. Delegates handling of these to member overloads that
|
||||
// inheriting classes can handle.
|
||||
@@ -77,13 +75,13 @@ protected:
|
||||
// Called when Destroy is called.
|
||||
virtual void OnDestroy();
|
||||
|
||||
private:
|
||||
private:
|
||||
friend class WindowClassRegistrar;
|
||||
|
||||
// OS callback called by message pump. Handles the WM_NCCREATE message which
|
||||
// is passed when the non-client area is being created and enables automatic
|
||||
// non-client DPI scaling so that the non-client area automatically
|
||||
// responsponds to changes in DPI. All other messages are handled by
|
||||
// responds to changes in DPI. All other messages are handled by
|
||||
// MessageHandler.
|
||||
static LRESULT CALLBACK WndProc(HWND const window,
|
||||
UINT const message,
|
||||
@@ -91,7 +89,10 @@ private:
|
||||
LPARAM const lparam) noexcept;
|
||||
|
||||
// Retrieves a class instance pointer for |window|
|
||||
static Win32Window *GetThisFromHandle(HWND const window) noexcept;
|
||||
static Win32Window* GetThisFromHandle(HWND const window) noexcept;
|
||||
|
||||
// Update the window frame's theme to match the system theme.
|
||||
static void UpdateTheme(HWND const window);
|
||||
|
||||
bool quit_on_close_ = false;
|
||||
|
||||
@@ -102,4 +103,4 @@ private:
|
||||
HWND child_content_ = nullptr;
|
||||
};
|
||||
|
||||
#endif // RUNNER_WIN32_WINDOW_H_
|
||||
#endif // RUNNER_WIN32_WINDOW_H_
|
||||
+20
-20
@@ -172,7 +172,7 @@ checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da"
|
||||
[[package]]
|
||||
name = "app-error"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=4643e568d2d9b725b0d06dfbbf351317cf9a2c9f#4643e568d2d9b725b0d06dfbbf351317cf9a2c9f"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=e31e541d07b8ef5e3f33b0b3dd54ebc22a79f86a#e31e541d07b8ef5e3f33b0b3dd54ebc22a79f86a"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bincode",
|
||||
@@ -192,7 +192,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "appflowy-ai-client"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=4643e568d2d9b725b0d06dfbbf351317cf9a2c9f#4643e568d2d9b725b0d06dfbbf351317cf9a2c9f"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=e31e541d07b8ef5e3f33b0b3dd54ebc22a79f86a#e31e541d07b8ef5e3f33b0b3dd54ebc22a79f86a"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bytes",
|
||||
@@ -888,7 +888,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "client-api"
|
||||
version = "0.2.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=4643e568d2d9b725b0d06dfbbf351317cf9a2c9f#4643e568d2d9b725b0d06dfbbf351317cf9a2c9f"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=e31e541d07b8ef5e3f33b0b3dd54ebc22a79f86a#e31e541d07b8ef5e3f33b0b3dd54ebc22a79f86a"
|
||||
dependencies = [
|
||||
"again",
|
||||
"anyhow",
|
||||
@@ -943,7 +943,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "client-api-entity"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=4643e568d2d9b725b0d06dfbbf351317cf9a2c9f#4643e568d2d9b725b0d06dfbbf351317cf9a2c9f"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=e31e541d07b8ef5e3f33b0b3dd54ebc22a79f86a#e31e541d07b8ef5e3f33b0b3dd54ebc22a79f86a"
|
||||
dependencies = [
|
||||
"collab-entity",
|
||||
"collab-rt-entity",
|
||||
@@ -956,7 +956,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "client-websocket"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=4643e568d2d9b725b0d06dfbbf351317cf9a2c9f#4643e568d2d9b725b0d06dfbbf351317cf9a2c9f"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=e31e541d07b8ef5e3f33b0b3dd54ebc22a79f86a#e31e541d07b8ef5e3f33b0b3dd54ebc22a79f86a"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"futures-util",
|
||||
@@ -1030,7 +1030,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "collab"
|
||||
version = "0.2.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=4175465382020f1bd9cd9aa0e0766178eac7b4e5#4175465382020f1bd9cd9aa0e0766178eac7b4e5"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=699d6e3b45b9ac23987c6eb5fc3df3e29f3cbf9d#699d6e3b45b9ac23987c6eb5fc3df3e29f3cbf9d"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"arc-swap",
|
||||
@@ -1055,7 +1055,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "collab-database"
|
||||
version = "0.2.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=4175465382020f1bd9cd9aa0e0766178eac7b4e5#4175465382020f1bd9cd9aa0e0766178eac7b4e5"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=699d6e3b45b9ac23987c6eb5fc3df3e29f3cbf9d#699d6e3b45b9ac23987c6eb5fc3df3e29f3cbf9d"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -1094,7 +1094,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "collab-document"
|
||||
version = "0.2.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=4175465382020f1bd9cd9aa0e0766178eac7b4e5#4175465382020f1bd9cd9aa0e0766178eac7b4e5"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=699d6e3b45b9ac23987c6eb5fc3df3e29f3cbf9d#699d6e3b45b9ac23987c6eb5fc3df3e29f3cbf9d"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"arc-swap",
|
||||
@@ -1115,7 +1115,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "collab-entity"
|
||||
version = "0.2.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=4175465382020f1bd9cd9aa0e0766178eac7b4e5#4175465382020f1bd9cd9aa0e0766178eac7b4e5"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=699d6e3b45b9ac23987c6eb5fc3df3e29f3cbf9d#699d6e3b45b9ac23987c6eb5fc3df3e29f3cbf9d"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bytes",
|
||||
@@ -1135,7 +1135,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "collab-folder"
|
||||
version = "0.2.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=4175465382020f1bd9cd9aa0e0766178eac7b4e5#4175465382020f1bd9cd9aa0e0766178eac7b4e5"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=699d6e3b45b9ac23987c6eb5fc3df3e29f3cbf9d#699d6e3b45b9ac23987c6eb5fc3df3e29f3cbf9d"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"arc-swap",
|
||||
@@ -1157,7 +1157,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "collab-importer"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=4175465382020f1bd9cd9aa0e0766178eac7b4e5#4175465382020f1bd9cd9aa0e0766178eac7b4e5"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=699d6e3b45b9ac23987c6eb5fc3df3e29f3cbf9d#699d6e3b45b9ac23987c6eb5fc3df3e29f3cbf9d"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-recursion",
|
||||
@@ -1218,7 +1218,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "collab-plugins"
|
||||
version = "0.2.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=4175465382020f1bd9cd9aa0e0766178eac7b4e5#4175465382020f1bd9cd9aa0e0766178eac7b4e5"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=699d6e3b45b9ac23987c6eb5fc3df3e29f3cbf9d#699d6e3b45b9ac23987c6eb5fc3df3e29f3cbf9d"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-stream",
|
||||
@@ -1256,7 +1256,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "collab-rt-entity"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=4643e568d2d9b725b0d06dfbbf351317cf9a2c9f#4643e568d2d9b725b0d06dfbbf351317cf9a2c9f"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=e31e541d07b8ef5e3f33b0b3dd54ebc22a79f86a#e31e541d07b8ef5e3f33b0b3dd54ebc22a79f86a"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bincode",
|
||||
@@ -1281,7 +1281,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "collab-rt-protocol"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=4643e568d2d9b725b0d06dfbbf351317cf9a2c9f#4643e568d2d9b725b0d06dfbbf351317cf9a2c9f"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=e31e541d07b8ef5e3f33b0b3dd54ebc22a79f86a#e31e541d07b8ef5e3f33b0b3dd54ebc22a79f86a"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -1298,7 +1298,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "collab-user"
|
||||
version = "0.2.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=4175465382020f1bd9cd9aa0e0766178eac7b4e5#4175465382020f1bd9cd9aa0e0766178eac7b4e5"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=699d6e3b45b9ac23987c6eb5fc3df3e29f3cbf9d#699d6e3b45b9ac23987c6eb5fc3df3e29f3cbf9d"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"collab",
|
||||
@@ -1678,7 +1678,7 @@ checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308"
|
||||
[[package]]
|
||||
name = "database-entity"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=4643e568d2d9b725b0d06dfbbf351317cf9a2c9f#4643e568d2d9b725b0d06dfbbf351317cf9a2c9f"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=e31e541d07b8ef5e3f33b0b3dd54ebc22a79f86a#e31e541d07b8ef5e3f33b0b3dd54ebc22a79f86a"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"app-error",
|
||||
@@ -3265,7 +3265,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "gotrue"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=4643e568d2d9b725b0d06dfbbf351317cf9a2c9f#4643e568d2d9b725b0d06dfbbf351317cf9a2c9f"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=e31e541d07b8ef5e3f33b0b3dd54ebc22a79f86a#e31e541d07b8ef5e3f33b0b3dd54ebc22a79f86a"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"futures-util",
|
||||
@@ -3282,7 +3282,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "gotrue-entity"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=4643e568d2d9b725b0d06dfbbf351317cf9a2c9f#4643e568d2d9b725b0d06dfbbf351317cf9a2c9f"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=e31e541d07b8ef5e3f33b0b3dd54ebc22a79f86a#e31e541d07b8ef5e3f33b0b3dd54ebc22a79f86a"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"app-error",
|
||||
@@ -3714,7 +3714,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "infra"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=4643e568d2d9b725b0d06dfbbf351317cf9a2c9f#4643e568d2d9b725b0d06dfbbf351317cf9a2c9f"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=e31e541d07b8ef5e3f33b0b3dd54ebc22a79f86a#e31e541d07b8ef5e3f33b0b3dd54ebc22a79f86a"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bytes",
|
||||
@@ -6398,7 +6398,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "shared-entity"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=4643e568d2d9b725b0d06dfbbf351317cf9a2c9f#4643e568d2d9b725b0d06dfbbf351317cf9a2c9f"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=e31e541d07b8ef5e3f33b0b3dd54ebc22a79f86a#e31e541d07b8ef5e3f33b0b3dd54ebc22a79f86a"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"app-error",
|
||||
|
||||
@@ -120,14 +120,14 @@ custom-protocol = ["tauri/custom-protocol"]
|
||||
# To switch to the local path, run:
|
||||
# scripts/tool/update_collab_source.sh
|
||||
# ⚠️⚠️⚠️️
|
||||
collab = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "4175465382020f1bd9cd9aa0e0766178eac7b4e5" }
|
||||
collab-entity = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "4175465382020f1bd9cd9aa0e0766178eac7b4e5" }
|
||||
collab-folder = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "4175465382020f1bd9cd9aa0e0766178eac7b4e5" }
|
||||
collab-document = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "4175465382020f1bd9cd9aa0e0766178eac7b4e5" }
|
||||
collab-database = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "4175465382020f1bd9cd9aa0e0766178eac7b4e5" }
|
||||
collab-plugins = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "4175465382020f1bd9cd9aa0e0766178eac7b4e5" }
|
||||
collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "4175465382020f1bd9cd9aa0e0766178eac7b4e5" }
|
||||
collab-importer = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "4175465382020f1bd9cd9aa0e0766178eac7b4e5" }
|
||||
collab = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "699d6e3b45b9ac23987c6eb5fc3df3e29f3cbf9d" }
|
||||
collab-entity = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "699d6e3b45b9ac23987c6eb5fc3df3e29f3cbf9d" }
|
||||
collab-folder = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "699d6e3b45b9ac23987c6eb5fc3df3e29f3cbf9d" }
|
||||
collab-document = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "699d6e3b45b9ac23987c6eb5fc3df3e29f3cbf9d" }
|
||||
collab-database = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "699d6e3b45b9ac23987c6eb5fc3df3e29f3cbf9d" }
|
||||
collab-plugins = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "699d6e3b45b9ac23987c6eb5fc3df3e29f3cbf9d" }
|
||||
collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "699d6e3b45b9ac23987c6eb5fc3df3e29f3cbf9d" }
|
||||
collab-importer = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "699d6e3b45b9ac23987c6eb5fc3df3e29f3cbf9d" }
|
||||
|
||||
# Working directory: frontend
|
||||
# To update the commit ID, run:
|
||||
|
||||
+20
-20
@@ -163,7 +163,7 @@ checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da"
|
||||
[[package]]
|
||||
name = "app-error"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=4643e568d2d9b725b0d06dfbbf351317cf9a2c9f#4643e568d2d9b725b0d06dfbbf351317cf9a2c9f"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=e31e541d07b8ef5e3f33b0b3dd54ebc22a79f86a#e31e541d07b8ef5e3f33b0b3dd54ebc22a79f86a"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bincode",
|
||||
@@ -183,7 +183,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "appflowy-ai-client"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=4643e568d2d9b725b0d06dfbbf351317cf9a2c9f#4643e568d2d9b725b0d06dfbbf351317cf9a2c9f"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=e31e541d07b8ef5e3f33b0b3dd54ebc22a79f86a#e31e541d07b8ef5e3f33b0b3dd54ebc22a79f86a"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bytes",
|
||||
@@ -877,7 +877,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "client-api"
|
||||
version = "0.2.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=4643e568d2d9b725b0d06dfbbf351317cf9a2c9f#4643e568d2d9b725b0d06dfbbf351317cf9a2c9f"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=e31e541d07b8ef5e3f33b0b3dd54ebc22a79f86a#e31e541d07b8ef5e3f33b0b3dd54ebc22a79f86a"
|
||||
dependencies = [
|
||||
"again",
|
||||
"anyhow",
|
||||
@@ -932,7 +932,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "client-api-entity"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=4643e568d2d9b725b0d06dfbbf351317cf9a2c9f#4643e568d2d9b725b0d06dfbbf351317cf9a2c9f"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=e31e541d07b8ef5e3f33b0b3dd54ebc22a79f86a#e31e541d07b8ef5e3f33b0b3dd54ebc22a79f86a"
|
||||
dependencies = [
|
||||
"collab-entity",
|
||||
"collab-rt-entity",
|
||||
@@ -945,7 +945,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "client-websocket"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=4643e568d2d9b725b0d06dfbbf351317cf9a2c9f#4643e568d2d9b725b0d06dfbbf351317cf9a2c9f"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=e31e541d07b8ef5e3f33b0b3dd54ebc22a79f86a#e31e541d07b8ef5e3f33b0b3dd54ebc22a79f86a"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"futures-util",
|
||||
@@ -1028,7 +1028,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "collab"
|
||||
version = "0.2.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=4175465382020f1bd9cd9aa0e0766178eac7b4e5#4175465382020f1bd9cd9aa0e0766178eac7b4e5"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=699d6e3b45b9ac23987c6eb5fc3df3e29f3cbf9d#699d6e3b45b9ac23987c6eb5fc3df3e29f3cbf9d"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"arc-swap",
|
||||
@@ -1053,7 +1053,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "collab-database"
|
||||
version = "0.2.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=4175465382020f1bd9cd9aa0e0766178eac7b4e5#4175465382020f1bd9cd9aa0e0766178eac7b4e5"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=699d6e3b45b9ac23987c6eb5fc3df3e29f3cbf9d#699d6e3b45b9ac23987c6eb5fc3df3e29f3cbf9d"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -1092,7 +1092,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "collab-document"
|
||||
version = "0.2.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=4175465382020f1bd9cd9aa0e0766178eac7b4e5#4175465382020f1bd9cd9aa0e0766178eac7b4e5"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=699d6e3b45b9ac23987c6eb5fc3df3e29f3cbf9d#699d6e3b45b9ac23987c6eb5fc3df3e29f3cbf9d"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"arc-swap",
|
||||
@@ -1113,7 +1113,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "collab-entity"
|
||||
version = "0.2.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=4175465382020f1bd9cd9aa0e0766178eac7b4e5#4175465382020f1bd9cd9aa0e0766178eac7b4e5"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=699d6e3b45b9ac23987c6eb5fc3df3e29f3cbf9d#699d6e3b45b9ac23987c6eb5fc3df3e29f3cbf9d"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bytes",
|
||||
@@ -1133,7 +1133,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "collab-folder"
|
||||
version = "0.2.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=4175465382020f1bd9cd9aa0e0766178eac7b4e5#4175465382020f1bd9cd9aa0e0766178eac7b4e5"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=699d6e3b45b9ac23987c6eb5fc3df3e29f3cbf9d#699d6e3b45b9ac23987c6eb5fc3df3e29f3cbf9d"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"arc-swap",
|
||||
@@ -1155,7 +1155,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "collab-importer"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=4175465382020f1bd9cd9aa0e0766178eac7b4e5#4175465382020f1bd9cd9aa0e0766178eac7b4e5"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=699d6e3b45b9ac23987c6eb5fc3df3e29f3cbf9d#699d6e3b45b9ac23987c6eb5fc3df3e29f3cbf9d"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-recursion",
|
||||
@@ -1216,7 +1216,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "collab-plugins"
|
||||
version = "0.2.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=4175465382020f1bd9cd9aa0e0766178eac7b4e5#4175465382020f1bd9cd9aa0e0766178eac7b4e5"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=699d6e3b45b9ac23987c6eb5fc3df3e29f3cbf9d#699d6e3b45b9ac23987c6eb5fc3df3e29f3cbf9d"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-stream",
|
||||
@@ -1254,7 +1254,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "collab-rt-entity"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=4643e568d2d9b725b0d06dfbbf351317cf9a2c9f#4643e568d2d9b725b0d06dfbbf351317cf9a2c9f"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=e31e541d07b8ef5e3f33b0b3dd54ebc22a79f86a#e31e541d07b8ef5e3f33b0b3dd54ebc22a79f86a"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bincode",
|
||||
@@ -1279,7 +1279,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "collab-rt-protocol"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=4643e568d2d9b725b0d06dfbbf351317cf9a2c9f#4643e568d2d9b725b0d06dfbbf351317cf9a2c9f"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=e31e541d07b8ef5e3f33b0b3dd54ebc22a79f86a#e31e541d07b8ef5e3f33b0b3dd54ebc22a79f86a"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -1296,7 +1296,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "collab-user"
|
||||
version = "0.2.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=4175465382020f1bd9cd9aa0e0766178eac7b4e5#4175465382020f1bd9cd9aa0e0766178eac7b4e5"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=699d6e3b45b9ac23987c6eb5fc3df3e29f3cbf9d#699d6e3b45b9ac23987c6eb5fc3df3e29f3cbf9d"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"collab",
|
||||
@@ -1683,7 +1683,7 @@ checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5"
|
||||
[[package]]
|
||||
name = "database-entity"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=4643e568d2d9b725b0d06dfbbf351317cf9a2c9f#4643e568d2d9b725b0d06dfbbf351317cf9a2c9f"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=e31e541d07b8ef5e3f33b0b3dd54ebc22a79f86a#e31e541d07b8ef5e3f33b0b3dd54ebc22a79f86a"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"app-error",
|
||||
@@ -3347,7 +3347,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "gotrue"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=4643e568d2d9b725b0d06dfbbf351317cf9a2c9f#4643e568d2d9b725b0d06dfbbf351317cf9a2c9f"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=e31e541d07b8ef5e3f33b0b3dd54ebc22a79f86a#e31e541d07b8ef5e3f33b0b3dd54ebc22a79f86a"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"futures-util",
|
||||
@@ -3364,7 +3364,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "gotrue-entity"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=4643e568d2d9b725b0d06dfbbf351317cf9a2c9f#4643e568d2d9b725b0d06dfbbf351317cf9a2c9f"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=e31e541d07b8ef5e3f33b0b3dd54ebc22a79f86a#e31e541d07b8ef5e3f33b0b3dd54ebc22a79f86a"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"app-error",
|
||||
@@ -3801,7 +3801,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "infra"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=4643e568d2d9b725b0d06dfbbf351317cf9a2c9f#4643e568d2d9b725b0d06dfbbf351317cf9a2c9f"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=e31e541d07b8ef5e3f33b0b3dd54ebc22a79f86a#e31e541d07b8ef5e3f33b0b3dd54ebc22a79f86a"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bytes",
|
||||
@@ -6478,7 +6478,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "shared-entity"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=4643e568d2d9b725b0d06dfbbf351317cf9a2c9f#4643e568d2d9b725b0d06dfbbf351317cf9a2c9f"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=e31e541d07b8ef5e3f33b0b3dd54ebc22a79f86a#e31e541d07b8ef5e3f33b0b3dd54ebc22a79f86a"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"app-error",
|
||||
|
||||
@@ -118,14 +118,14 @@ custom-protocol = ["tauri/custom-protocol"]
|
||||
# To switch to the local path, run:
|
||||
# scripts/tool/update_collab_source.sh
|
||||
# ⚠️⚠️⚠️️
|
||||
collab = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "4175465382020f1bd9cd9aa0e0766178eac7b4e5" }
|
||||
collab-entity = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "4175465382020f1bd9cd9aa0e0766178eac7b4e5" }
|
||||
collab-folder = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "4175465382020f1bd9cd9aa0e0766178eac7b4e5" }
|
||||
collab-document = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "4175465382020f1bd9cd9aa0e0766178eac7b4e5" }
|
||||
collab-database = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "4175465382020f1bd9cd9aa0e0766178eac7b4e5" }
|
||||
collab-plugins = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "4175465382020f1bd9cd9aa0e0766178eac7b4e5" }
|
||||
collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "4175465382020f1bd9cd9aa0e0766178eac7b4e5" }
|
||||
collab-importer = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "4175465382020f1bd9cd9aa0e0766178eac7b4e5" }
|
||||
collab = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "699d6e3b45b9ac23987c6eb5fc3df3e29f3cbf9d" }
|
||||
collab-entity = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "699d6e3b45b9ac23987c6eb5fc3df3e29f3cbf9d" }
|
||||
collab-folder = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "699d6e3b45b9ac23987c6eb5fc3df3e29f3cbf9d" }
|
||||
collab-document = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "699d6e3b45b9ac23987c6eb5fc3df3e29f3cbf9d" }
|
||||
collab-database = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "699d6e3b45b9ac23987c6eb5fc3df3e29f3cbf9d" }
|
||||
collab-plugins = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "699d6e3b45b9ac23987c6eb5fc3df3e29f3cbf9d" }
|
||||
collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "699d6e3b45b9ac23987c6eb5fc3df3e29f3cbf9d" }
|
||||
collab-importer = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "699d6e3b45b9ac23987c6eb5fc3df3e29f3cbf9d" }
|
||||
|
||||
|
||||
# Working directory: frontend
|
||||
|
||||
@@ -90,11 +90,11 @@ function AcceptInvitationPage () {
|
||||
>
|
||||
{t('invitation.join')}
|
||||
{' '}
|
||||
<span className={'font-semibold'}>{currentUser?.name}</span>
|
||||
<span className={'font-semibold'}>{invitation?.workspace_name}</span>
|
||||
{' '}
|
||||
{t('invitation.on')}
|
||||
{' '}
|
||||
<span className={'whitespace-nowrap'}>{invitation?.workspace_name}</span>
|
||||
<span className={'whitespace-nowrap'}>AppFlowy</span>
|
||||
|
||||
</div>
|
||||
<Divider className={'max-w-full w-[400px]'} />
|
||||
|
||||
@@ -445,7 +445,7 @@
|
||||
"removeHomepage": "Remove homepage",
|
||||
"selectHomePage": "Select a page",
|
||||
"clearHomePage": "Clear the home page for this namespace",
|
||||
"customUrl": "Custom url",
|
||||
"customUrl": "Custom URL",
|
||||
"namespace": {
|
||||
"description": "This change will apply to all the published pages live on this namespace",
|
||||
"tooltip": "We reserve the rights to remove any inappropriate namespaces",
|
||||
@@ -935,9 +935,11 @@
|
||||
"itemFive": "Mobile app",
|
||||
"itemSix": "AI Responses",
|
||||
"itemFileUpload": "File uploads",
|
||||
"customNamespace": "Custom namespace",
|
||||
"tooltipSix": "Lifetime means the number of responses never reset",
|
||||
"intelligentSearch": "Intelligent search",
|
||||
"tooltipSeven": "Allows you to customize part of the URL for your workspace"
|
||||
"tooltipSeven": "Allows you to customize part of the URL for your workspace",
|
||||
"customNamespaceTooltip": "Custom published site URL"
|
||||
},
|
||||
"freeLabels": {
|
||||
"itemOne": "Charged per workspace",
|
||||
@@ -1648,14 +1650,15 @@
|
||||
"file": "File"
|
||||
},
|
||||
"subPage": {
|
||||
"name": "Insert page",
|
||||
"name": "Document",
|
||||
"keyword1": "sub page",
|
||||
"keyword2": "page",
|
||||
"keyword3": "child page",
|
||||
"keyword4": "insert page",
|
||||
"keyword5": "embed page",
|
||||
"keyword6": "new page",
|
||||
"keyword7": "create page"
|
||||
"keyword7": "create page",
|
||||
"keyword8": "document"
|
||||
}
|
||||
},
|
||||
"selectionMenu": {
|
||||
@@ -2825,4 +2828,4 @@
|
||||
"many": "{count} members",
|
||||
"other": "{count} members"
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
+20
-20
@@ -163,7 +163,7 @@ checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da"
|
||||
[[package]]
|
||||
name = "app-error"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=4643e568d2d9b725b0d06dfbbf351317cf9a2c9f#4643e568d2d9b725b0d06dfbbf351317cf9a2c9f"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=e31e541d07b8ef5e3f33b0b3dd54ebc22a79f86a#e31e541d07b8ef5e3f33b0b3dd54ebc22a79f86a"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bincode",
|
||||
@@ -183,7 +183,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "appflowy-ai-client"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=4643e568d2d9b725b0d06dfbbf351317cf9a2c9f#4643e568d2d9b725b0d06dfbbf351317cf9a2c9f"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=e31e541d07b8ef5e3f33b0b3dd54ebc22a79f86a#e31e541d07b8ef5e3f33b0b3dd54ebc22a79f86a"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bytes",
|
||||
@@ -780,7 +780,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "client-api"
|
||||
version = "0.2.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=4643e568d2d9b725b0d06dfbbf351317cf9a2c9f#4643e568d2d9b725b0d06dfbbf351317cf9a2c9f"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=e31e541d07b8ef5e3f33b0b3dd54ebc22a79f86a#e31e541d07b8ef5e3f33b0b3dd54ebc22a79f86a"
|
||||
dependencies = [
|
||||
"again",
|
||||
"anyhow",
|
||||
@@ -835,7 +835,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "client-api-entity"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=4643e568d2d9b725b0d06dfbbf351317cf9a2c9f#4643e568d2d9b725b0d06dfbbf351317cf9a2c9f"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=e31e541d07b8ef5e3f33b0b3dd54ebc22a79f86a#e31e541d07b8ef5e3f33b0b3dd54ebc22a79f86a"
|
||||
dependencies = [
|
||||
"collab-entity",
|
||||
"collab-rt-entity",
|
||||
@@ -848,7 +848,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "client-websocket"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=4643e568d2d9b725b0d06dfbbf351317cf9a2c9f#4643e568d2d9b725b0d06dfbbf351317cf9a2c9f"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=e31e541d07b8ef5e3f33b0b3dd54ebc22a79f86a#e31e541d07b8ef5e3f33b0b3dd54ebc22a79f86a"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"futures-util",
|
||||
@@ -891,7 +891,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "collab"
|
||||
version = "0.2.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=4175465382020f1bd9cd9aa0e0766178eac7b4e5#4175465382020f1bd9cd9aa0e0766178eac7b4e5"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=699d6e3b45b9ac23987c6eb5fc3df3e29f3cbf9d#699d6e3b45b9ac23987c6eb5fc3df3e29f3cbf9d"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"arc-swap",
|
||||
@@ -916,7 +916,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "collab-database"
|
||||
version = "0.2.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=4175465382020f1bd9cd9aa0e0766178eac7b4e5#4175465382020f1bd9cd9aa0e0766178eac7b4e5"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=699d6e3b45b9ac23987c6eb5fc3df3e29f3cbf9d#699d6e3b45b9ac23987c6eb5fc3df3e29f3cbf9d"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -955,7 +955,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "collab-document"
|
||||
version = "0.2.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=4175465382020f1bd9cd9aa0e0766178eac7b4e5#4175465382020f1bd9cd9aa0e0766178eac7b4e5"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=699d6e3b45b9ac23987c6eb5fc3df3e29f3cbf9d#699d6e3b45b9ac23987c6eb5fc3df3e29f3cbf9d"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"arc-swap",
|
||||
@@ -976,7 +976,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "collab-entity"
|
||||
version = "0.2.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=4175465382020f1bd9cd9aa0e0766178eac7b4e5#4175465382020f1bd9cd9aa0e0766178eac7b4e5"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=699d6e3b45b9ac23987c6eb5fc3df3e29f3cbf9d#699d6e3b45b9ac23987c6eb5fc3df3e29f3cbf9d"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bytes",
|
||||
@@ -996,7 +996,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "collab-folder"
|
||||
version = "0.2.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=4175465382020f1bd9cd9aa0e0766178eac7b4e5#4175465382020f1bd9cd9aa0e0766178eac7b4e5"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=699d6e3b45b9ac23987c6eb5fc3df3e29f3cbf9d#699d6e3b45b9ac23987c6eb5fc3df3e29f3cbf9d"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"arc-swap",
|
||||
@@ -1018,7 +1018,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "collab-importer"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=4175465382020f1bd9cd9aa0e0766178eac7b4e5#4175465382020f1bd9cd9aa0e0766178eac7b4e5"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=699d6e3b45b9ac23987c6eb5fc3df3e29f3cbf9d#699d6e3b45b9ac23987c6eb5fc3df3e29f3cbf9d"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-recursion",
|
||||
@@ -1079,7 +1079,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "collab-plugins"
|
||||
version = "0.2.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=4175465382020f1bd9cd9aa0e0766178eac7b4e5#4175465382020f1bd9cd9aa0e0766178eac7b4e5"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=699d6e3b45b9ac23987c6eb5fc3df3e29f3cbf9d#699d6e3b45b9ac23987c6eb5fc3df3e29f3cbf9d"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-stream",
|
||||
@@ -1117,7 +1117,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "collab-rt-entity"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=4643e568d2d9b725b0d06dfbbf351317cf9a2c9f#4643e568d2d9b725b0d06dfbbf351317cf9a2c9f"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=e31e541d07b8ef5e3f33b0b3dd54ebc22a79f86a#e31e541d07b8ef5e3f33b0b3dd54ebc22a79f86a"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bincode",
|
||||
@@ -1142,7 +1142,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "collab-rt-protocol"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=4643e568d2d9b725b0d06dfbbf351317cf9a2c9f#4643e568d2d9b725b0d06dfbbf351317cf9a2c9f"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=e31e541d07b8ef5e3f33b0b3dd54ebc22a79f86a#e31e541d07b8ef5e3f33b0b3dd54ebc22a79f86a"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -1159,7 +1159,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "collab-user"
|
||||
version = "0.2.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=4175465382020f1bd9cd9aa0e0766178eac7b4e5#4175465382020f1bd9cd9aa0e0766178eac7b4e5"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=699d6e3b45b9ac23987c6eb5fc3df3e29f3cbf9d#699d6e3b45b9ac23987c6eb5fc3df3e29f3cbf9d"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"collab",
|
||||
@@ -1537,7 +1537,7 @@ checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308"
|
||||
[[package]]
|
||||
name = "database-entity"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=4643e568d2d9b725b0d06dfbbf351317cf9a2c9f#4643e568d2d9b725b0d06dfbbf351317cf9a2c9f"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=e31e541d07b8ef5e3f33b0b3dd54ebc22a79f86a#e31e541d07b8ef5e3f33b0b3dd54ebc22a79f86a"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"app-error",
|
||||
@@ -2979,7 +2979,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "gotrue"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=4643e568d2d9b725b0d06dfbbf351317cf9a2c9f#4643e568d2d9b725b0d06dfbbf351317cf9a2c9f"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=e31e541d07b8ef5e3f33b0b3dd54ebc22a79f86a#e31e541d07b8ef5e3f33b0b3dd54ebc22a79f86a"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"futures-util",
|
||||
@@ -2996,7 +2996,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "gotrue-entity"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=4643e568d2d9b725b0d06dfbbf351317cf9a2c9f#4643e568d2d9b725b0d06dfbbf351317cf9a2c9f"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=e31e541d07b8ef5e3f33b0b3dd54ebc22a79f86a#e31e541d07b8ef5e3f33b0b3dd54ebc22a79f86a"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"app-error",
|
||||
@@ -3358,7 +3358,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "infra"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=4643e568d2d9b725b0d06dfbbf351317cf9a2c9f#4643e568d2d9b725b0d06dfbbf351317cf9a2c9f"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=e31e541d07b8ef5e3f33b0b3dd54ebc22a79f86a#e31e541d07b8ef5e3f33b0b3dd54ebc22a79f86a"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bytes",
|
||||
@@ -5663,7 +5663,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "shared-entity"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=4643e568d2d9b725b0d06dfbbf351317cf9a2c9f#4643e568d2d9b725b0d06dfbbf351317cf9a2c9f"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=e31e541d07b8ef5e3f33b0b3dd54ebc22a79f86a#e31e541d07b8ef5e3f33b0b3dd54ebc22a79f86a"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"app-error",
|
||||
|
||||
@@ -106,8 +106,8 @@ dashmap = "6.0.1"
|
||||
# Run the script.add_workspace_members:
|
||||
# scripts/tool/update_client_api_rev.sh new_rev_id
|
||||
# ⚠️⚠️⚠️️
|
||||
client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "4643e568d2d9b725b0d06dfbbf351317cf9a2c9f" }
|
||||
client-api-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "4643e568d2d9b725b0d06dfbbf351317cf9a2c9f" }
|
||||
client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "e31e541d07b8ef5e3f33b0b3dd54ebc22a79f86a" }
|
||||
client-api-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "e31e541d07b8ef5e3f33b0b3dd54ebc22a79f86a" }
|
||||
|
||||
[profile.dev]
|
||||
opt-level = 0
|
||||
@@ -142,14 +142,14 @@ rocksdb = { git = "https://github.com/rust-rocksdb/rust-rocksdb", rev = "1710120
|
||||
# To switch to the local path, run:
|
||||
# scripts/tool/update_collab_source.sh
|
||||
# ⚠️⚠️⚠️️
|
||||
collab = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "4175465382020f1bd9cd9aa0e0766178eac7b4e5" }
|
||||
collab-entity = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "4175465382020f1bd9cd9aa0e0766178eac7b4e5" }
|
||||
collab-folder = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "4175465382020f1bd9cd9aa0e0766178eac7b4e5" }
|
||||
collab-document = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "4175465382020f1bd9cd9aa0e0766178eac7b4e5" }
|
||||
collab-database = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "4175465382020f1bd9cd9aa0e0766178eac7b4e5" }
|
||||
collab-plugins = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "4175465382020f1bd9cd9aa0e0766178eac7b4e5" }
|
||||
collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "4175465382020f1bd9cd9aa0e0766178eac7b4e5" }
|
||||
collab-importer = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "4175465382020f1bd9cd9aa0e0766178eac7b4e5" }
|
||||
collab = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "699d6e3b45b9ac23987c6eb5fc3df3e29f3cbf9d" }
|
||||
collab-entity = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "699d6e3b45b9ac23987c6eb5fc3df3e29f3cbf9d" }
|
||||
collab-folder = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "699d6e3b45b9ac23987c6eb5fc3df3e29f3cbf9d" }
|
||||
collab-document = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "699d6e3b45b9ac23987c6eb5fc3df3e29f3cbf9d" }
|
||||
collab-database = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "699d6e3b45b9ac23987c6eb5fc3df3e29f3cbf9d" }
|
||||
collab-plugins = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "699d6e3b45b9ac23987c6eb5fc3df3e29f3cbf9d" }
|
||||
collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "699d6e3b45b9ac23987c6eb5fc3df3e29f3cbf9d" }
|
||||
collab-importer = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "699d6e3b45b9ac23987c6eb5fc3df3e29f3cbf9d" }
|
||||
|
||||
# Working directory: frontend
|
||||
# To update the commit ID, run:
|
||||
|
||||
@@ -411,7 +411,7 @@ impl LocalAIResourceController {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(level = "info", skip_all, err)]
|
||||
#[instrument(level = "info", skip_all)]
|
||||
pub fn get_chat_config(&self, rag_enabled: bool) -> FlowyResult<AIPluginConfig> {
|
||||
if !self.is_resource_ready() {
|
||||
return Err(FlowyError::local_ai().with_context("Local AI resources are not ready"));
|
||||
|
||||
@@ -29,8 +29,8 @@ collab-entity = { workspace = true }
|
||||
collab-plugins = { workspace = true }
|
||||
collab-folder = { workspace = true }
|
||||
|
||||
#collab = { workspace = true }
|
||||
collab = { workspace = true, features = ["verbose_log"] }
|
||||
collab = { workspace = true }
|
||||
#collab = { workspace = true, features = ["verbose_log"] }
|
||||
|
||||
diesel.workspace = true
|
||||
uuid.workspace = true
|
||||
|
||||
@@ -348,7 +348,7 @@ impl FolderCloudService for ServerProvider {
|
||||
async fn set_publish_namespace(
|
||||
&self,
|
||||
workspace_id: &str,
|
||||
new_namespace: &str,
|
||||
new_namespace: String,
|
||||
) -> Result<(), FlowyError> {
|
||||
let server = self.get_server()?;
|
||||
server
|
||||
|
||||
@@ -72,7 +72,7 @@ pub trait FolderCloudService: Send + Sync + 'static {
|
||||
async fn set_publish_namespace(
|
||||
&self,
|
||||
workspace_id: &str,
|
||||
new_namespace: &str,
|
||||
new_namespace: String,
|
||||
) -> Result<(), FlowyError>;
|
||||
|
||||
async fn list_published_views(
|
||||
|
||||
@@ -81,7 +81,7 @@ impl From<PublishInfo> for PublishInfoResponsePB {
|
||||
Self {
|
||||
view_id: info.view_id.to_string(),
|
||||
publish_name: info.publish_name,
|
||||
namespace: info.namespace,
|
||||
namespace: Some(info.namespace),
|
||||
publisher_email: info.publisher_email,
|
||||
publish_timestamp_sec: info.publish_timestamp.timestamp(),
|
||||
}
|
||||
|
||||
@@ -1298,11 +1298,11 @@ impl FolderManager {
|
||||
/// Get the namespace of the current workspace.
|
||||
/// The namespace is used to generate the URL of the published view.
|
||||
#[tracing::instrument(level = "debug", skip(self), err)]
|
||||
pub async fn set_publish_namespace(&self, namespace: String) -> FlowyResult<()> {
|
||||
pub async fn set_publish_namespace(&self, new_namespace: String) -> FlowyResult<()> {
|
||||
let workspace_id = self.user.workspace_id()?;
|
||||
self
|
||||
.cloud_service
|
||||
.set_publish_namespace(workspace_id.as_str(), namespace.as_str())
|
||||
.set_publish_namespace(workspace_id.as_str(), new_namespace)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -268,13 +268,12 @@ where
|
||||
async fn set_publish_namespace(
|
||||
&self,
|
||||
workspace_id: &str,
|
||||
new_namespace: &str,
|
||||
new_namespace: String,
|
||||
) -> Result<(), FlowyError> {
|
||||
let workspace_id = workspace_id.to_string();
|
||||
let namespace = new_namespace.to_string();
|
||||
let try_get_client = self.inner.try_get_client();
|
||||
try_get_client?
|
||||
.set_workspace_publish_namespace(&workspace_id, &namespace)
|
||||
.set_workspace_publish_namespace(&workspace_id, new_namespace)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -98,7 +98,7 @@ impl FolderCloudService for LocalServerFolderCloudServiceImpl {
|
||||
async fn set_publish_namespace(
|
||||
&self,
|
||||
_workspace_id: &str,
|
||||
_new_namespace: &str,
|
||||
_new_namespace: String,
|
||||
) -> Result<(), FlowyError> {
|
||||
Err(FlowyError::local_version_not_support())
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ use serde::Serialize;
|
||||
use std::fmt::Display;
|
||||
use std::ops::{Deref, DerefMut};
|
||||
use tokio::sync::broadcast;
|
||||
use tracing::warn;
|
||||
|
||||
#[async_trait]
|
||||
pub trait StorageService: Send + Sync {
|
||||
@@ -68,22 +67,25 @@ pub enum FileUploadState {
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
pub struct FileProgress {
|
||||
pub file_url: String,
|
||||
pub file_id: String,
|
||||
pub progress: f64,
|
||||
pub error: Option<String>,
|
||||
}
|
||||
|
||||
impl FileProgress {
|
||||
pub fn new_progress(file_url: String, progress: f64) -> Self {
|
||||
pub fn new_progress(file_url: String, file_id: String, progress: f64) -> Self {
|
||||
FileProgress {
|
||||
file_url,
|
||||
progress,
|
||||
file_id,
|
||||
progress: (progress * 10.0).round() / 10.0,
|
||||
error: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_error(file_url: String, error: String) -> Self {
|
||||
pub fn new_error(file_url: String, file_id: String, error: String) -> Self {
|
||||
FileProgress {
|
||||
file_url,
|
||||
file_id,
|
||||
progress: 0.0,
|
||||
error: Some(error),
|
||||
}
|
||||
@@ -92,7 +94,7 @@ impl FileProgress {
|
||||
|
||||
impl Display for FileProgress {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "FileProgress: {} - {}", self.file_url, self.progress)
|
||||
write!(f, "FileProgress: {} - {}", self.file_id, self.progress)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,9 +124,7 @@ impl ProgressNotifier {
|
||||
|
||||
pub async fn notify(&mut self, progress: FileUploadState) {
|
||||
self.current_value = Some(progress.clone());
|
||||
if let Err(err) = self.tx.send(progress) {
|
||||
warn!("Failed to send progress notification: {:?}", err);
|
||||
}
|
||||
let _ = self.tx.send(progress);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,9 +2,9 @@ use crate::entities::FileStatePB;
|
||||
use crate::file_cache::FileTempStorage;
|
||||
use crate::notification::{make_notification, StorageNotification};
|
||||
use crate::sqlite_sql::{
|
||||
batch_select_upload_file, delete_upload_file, insert_upload_file, insert_upload_part,
|
||||
is_upload_completed, select_upload_file, select_upload_parts, update_upload_file_completed,
|
||||
update_upload_file_upload_id, UploadFilePartTable, UploadFileTable,
|
||||
batch_select_upload_file, delete_all_upload_parts, delete_upload_file, insert_upload_file,
|
||||
insert_upload_part, is_upload_completed, select_upload_file, select_upload_parts,
|
||||
update_upload_file_completed, update_upload_file_upload_id, UploadFilePartTable, UploadFileTable,
|
||||
};
|
||||
use crate::uploader::{FileUploader, FileUploaderRunner, Signal, UploadTask, UploadTaskQueue};
|
||||
use allo_isolate::Isolate;
|
||||
@@ -64,7 +64,7 @@ impl StorageManager {
|
||||
"{}/cache_files",
|
||||
user_service.get_application_root_dir()
|
||||
));
|
||||
let (global_notifier, _) = broadcast::channel(1000);
|
||||
let (global_notifier, _) = broadcast::channel(2000);
|
||||
let temp_storage = Arc::new(FileTempStorage::new(temp_storage_path));
|
||||
let (notifier, notifier_rx) = watch::channel(Signal::Proceed);
|
||||
let task_queue = Arc::new(UploadTaskQueue::new(notifier));
|
||||
@@ -101,6 +101,31 @@ impl StorageManager {
|
||||
}
|
||||
});
|
||||
|
||||
let mut rx = global_notifier.subscribe();
|
||||
let weak_notifier = Arc::downgrade(&progress_notifiers);
|
||||
tokio::spawn(async move {
|
||||
while let Ok(progress) = rx.recv().await {
|
||||
if let Some(notifiers) = weak_notifier.upgrade() {
|
||||
if let Some(mut notifier) = notifiers.get_mut(&progress.file_id) {
|
||||
if progress.progress >= 1.0 {
|
||||
let finish = FileUploadState::Finished {
|
||||
file_id: progress.file_id,
|
||||
};
|
||||
notifier.notify(finish).await;
|
||||
} else {
|
||||
let progress = FileUploadState::Uploading {
|
||||
progress: progress.progress,
|
||||
};
|
||||
notifier.notify(progress).await;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
info!("progress notifiers is dropped");
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Self {
|
||||
storage_service,
|
||||
cloud_service,
|
||||
@@ -139,6 +164,7 @@ impl StorageManager {
|
||||
|
||||
if let Err(err) = self.global_notifier.send(FileProgress::new_progress(
|
||||
url.to_string(),
|
||||
file_id.clone(),
|
||||
if is_finish { 1.0 } else { 0.0 },
|
||||
)) {
|
||||
error!("[File] send global notifier failed: {}", err);
|
||||
@@ -194,7 +220,7 @@ async fn prepare_upload_task(
|
||||
) -> FlowyResult<()> {
|
||||
let uid = user_service.user_id()?;
|
||||
let conn = user_service.sqlite_connection(uid)?;
|
||||
let upload_files = batch_select_upload_file(conn, 100)?;
|
||||
let upload_files = batch_select_upload_file(conn, 100, false)?;
|
||||
let tasks = upload_files
|
||||
.into_iter()
|
||||
.map(|upload_file| UploadTask::BackgroundTask {
|
||||
@@ -205,7 +231,7 @@ async fn prepare_upload_task(
|
||||
retry_count: 0,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
info!("prepare upload task: {}", tasks.len());
|
||||
info!("[File] prepare upload task: {}", tasks.len());
|
||||
uploader.queue_tasks(tasks).await;
|
||||
Ok(())
|
||||
}
|
||||
@@ -317,7 +343,6 @@ impl StorageService for StorageServiceImpl {
|
||||
|
||||
// 1. create a file record and chunk the file
|
||||
let (chunks, record) = create_upload_record(workspace_id, parent_dir, local_file_path).await?;
|
||||
|
||||
// 2. save the record to sqlite
|
||||
let conn = self
|
||||
.user_service
|
||||
@@ -359,7 +384,7 @@ impl StorageService for StorageServiceImpl {
|
||||
},
|
||||
Err(err) => {
|
||||
if matches!(err.code, ErrorCode::DuplicateSqliteRecord) {
|
||||
info!("upload record already exists, skip creating new upload task");
|
||||
info!("[File] upload record already exists, skip creating new upload task");
|
||||
Ok::<_, FlowyError>((CreatedUpload { url, file_id }, None))
|
||||
} else {
|
||||
Err(err)
|
||||
@@ -373,19 +398,16 @@ impl StorageService for StorageServiceImpl {
|
||||
FlowyError::internal().with_context("failed to downcast record to UploadFileTable")
|
||||
})?;
|
||||
|
||||
if let Err(err) = start_upload(
|
||||
start_upload(
|
||||
&self.cloud_service,
|
||||
&self.user_service,
|
||||
&self.temp_storage,
|
||||
chunks,
|
||||
file_record,
|
||||
self.progress_notifiers.clone(),
|
||||
self.global_notifier.clone(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
error!("[File] start upload failed: {}", err);
|
||||
}
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -406,7 +428,6 @@ impl StorageService for StorageServiceImpl {
|
||||
&self.user_service,
|
||||
&self.temp_storage,
|
||||
upload_file,
|
||||
self.progress_notifiers.clone(),
|
||||
self.global_notifier.clone(),
|
||||
)
|
||||
.await?;
|
||||
@@ -462,6 +483,7 @@ async fn create_upload_record(
|
||||
let record = UploadFileTable {
|
||||
workspace_id,
|
||||
file_id,
|
||||
// When the upload_id is empty string, we will create a new upload using [Self::start_upload] method
|
||||
upload_id: "".to_string(),
|
||||
parent_dir,
|
||||
local_file_path,
|
||||
@@ -481,7 +503,6 @@ async fn start_upload(
|
||||
temp_storage: &Arc<FileTempStorage>,
|
||||
mut chunked_bytes: ChunkedBytes,
|
||||
upload_file: &UploadFileTable,
|
||||
progress_notifiers: Arc<DashMap<String, ProgressNotifier>>,
|
||||
global_notifier: GlobalNotifier,
|
||||
) -> FlowyResult<()> {
|
||||
// 4. gather existing completed parts
|
||||
@@ -496,6 +517,7 @@ async fn start_upload(
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let upload_offset = completed_parts.len() as i32;
|
||||
let total_parts = chunked_bytes.iter().count();
|
||||
chunked_bytes.set_current_offset(upload_offset);
|
||||
|
||||
info!(
|
||||
@@ -504,68 +526,72 @@ async fn start_upload(
|
||||
);
|
||||
|
||||
let mut upload_file = upload_file.clone();
|
||||
if upload_file.upload_id.is_empty() {
|
||||
// 1. create upload
|
||||
trace!(
|
||||
"[File] create upload for workspace: {}, parent_dir: {}, file_id: {}",
|
||||
upload_file.workspace_id,
|
||||
upload_file.parent_dir,
|
||||
upload_file.file_id
|
||||
);
|
||||
// 1. create upload
|
||||
trace!(
|
||||
"[File] create upload for workspace: {}, parent_dir: {}, file_id: {}",
|
||||
upload_file.workspace_id,
|
||||
upload_file.parent_dir,
|
||||
upload_file.file_id
|
||||
);
|
||||
|
||||
let create_upload_resp_result = cloud_service
|
||||
.create_upload(
|
||||
&upload_file.workspace_id,
|
||||
&upload_file.parent_dir,
|
||||
&upload_file.file_id,
|
||||
&upload_file.content_type,
|
||||
)
|
||||
.await;
|
||||
if let Err(err) = create_upload_resp_result.as_ref() {
|
||||
if err.is_file_limit_exceeded() {
|
||||
make_notification(StorageNotification::FileStorageLimitExceeded)
|
||||
.payload(err.clone())
|
||||
.send();
|
||||
}
|
||||
}
|
||||
let create_upload_resp = create_upload_resp_result?;
|
||||
|
||||
// 2. update upload_id
|
||||
let conn = user_service.sqlite_connection(user_service.user_id()?)?;
|
||||
update_upload_file_upload_id(
|
||||
conn,
|
||||
let create_upload_resp_result = cloud_service
|
||||
.create_upload(
|
||||
&upload_file.workspace_id,
|
||||
&upload_file.parent_dir,
|
||||
&upload_file.file_id,
|
||||
&create_upload_resp.upload_id,
|
||||
)?;
|
||||
|
||||
trace!(
|
||||
"[File] {} update upload_id: {}",
|
||||
upload_file.file_id,
|
||||
create_upload_resp.upload_id
|
||||
);
|
||||
// temporary store the upload_id
|
||||
upload_file.upload_id = create_upload_resp.upload_id;
|
||||
&upload_file.content_type,
|
||||
)
|
||||
.await;
|
||||
if let Err(err) = create_upload_resp_result.as_ref() {
|
||||
if err.is_file_limit_exceeded() {
|
||||
make_notification(StorageNotification::FileStorageLimitExceeded)
|
||||
.payload(err.clone())
|
||||
.send();
|
||||
}
|
||||
}
|
||||
let create_upload_resp = create_upload_resp_result?;
|
||||
|
||||
// 2. update upload_id
|
||||
let conn = user_service.sqlite_connection(user_service.user_id()?)?;
|
||||
update_upload_file_upload_id(
|
||||
conn,
|
||||
&upload_file.workspace_id,
|
||||
&upload_file.parent_dir,
|
||||
&upload_file.file_id,
|
||||
&create_upload_resp.upload_id,
|
||||
)?;
|
||||
|
||||
trace!(
|
||||
"[File] {} update upload_id: {}",
|
||||
upload_file.file_id,
|
||||
create_upload_resp.upload_id
|
||||
);
|
||||
upload_file.upload_id = create_upload_resp.upload_id;
|
||||
|
||||
// 3. start uploading parts
|
||||
trace!(
|
||||
"[File] {} start uploading parts: {}",
|
||||
info!(
|
||||
"[File] {} start uploading parts:{}, offset:{}",
|
||||
upload_file.file_id,
|
||||
chunked_bytes.iter().count()
|
||||
chunked_bytes.iter().count(),
|
||||
upload_offset,
|
||||
);
|
||||
let total_parts = chunked_bytes.iter().count();
|
||||
let iter = chunked_bytes.iter().enumerate();
|
||||
|
||||
for (index, chunk_bytes) in iter {
|
||||
let part_number = upload_offset + index as i32 + 1;
|
||||
trace!(
|
||||
info!(
|
||||
"[File] {} uploading {}th part, size:{}KB",
|
||||
upload_file.file_id,
|
||||
part_number,
|
||||
chunk_bytes.len() / 1000,
|
||||
);
|
||||
|
||||
let file_url = cloud_service
|
||||
.get_object_url_v1(
|
||||
&upload_file.workspace_id,
|
||||
&upload_file.parent_dir,
|
||||
&upload_file.file_id,
|
||||
)
|
||||
.await?;
|
||||
// start uploading parts
|
||||
match upload_part(
|
||||
cloud_service,
|
||||
@@ -580,29 +606,17 @@ async fn start_upload(
|
||||
.await
|
||||
{
|
||||
Ok(resp) => {
|
||||
let progress = (part_number as f64 / total_parts as f64).clamp(0.0, 1.0);
|
||||
trace!(
|
||||
"[File] {} upload progress: {}",
|
||||
upload_file.file_id,
|
||||
progress
|
||||
);
|
||||
|
||||
let file_url = cloud_service
|
||||
.get_object_url_v1(
|
||||
&upload_file.workspace_id,
|
||||
&upload_file.parent_dir,
|
||||
&upload_file.file_id,
|
||||
)
|
||||
.await?;
|
||||
|
||||
if let Err(err) = global_notifier.send(FileProgress::new_progress(file_url, progress)) {
|
||||
error!("[File] send global notifier failed: {}", err);
|
||||
let mut progress_value = (part_number as f64 / total_parts as f64).clamp(0.0, 1.0);
|
||||
// The 0.1 is reserved for the complete_upload progress
|
||||
if progress_value >= 0.9 {
|
||||
progress_value = 0.9;
|
||||
}
|
||||
let progress =
|
||||
FileProgress::new_progress(file_url, upload_file.file_id.clone(), progress_value);
|
||||
trace!("[File] upload progress: {}", progress);
|
||||
|
||||
if let Some(mut notifier) = progress_notifiers.get_mut(&upload_file.file_id) {
|
||||
notifier
|
||||
.notify(FileUploadState::Uploading { progress })
|
||||
.await;
|
||||
if let Err(err) = global_notifier.send(progress) {
|
||||
error!("[File] send global notifier failed: {}", err);
|
||||
}
|
||||
|
||||
// gather completed part
|
||||
@@ -618,7 +632,13 @@ async fn start_upload(
|
||||
.send();
|
||||
}
|
||||
|
||||
error!("[File] {} upload part failed: {}", upload_file.file_id, err);
|
||||
if let Err(err) = global_notifier.send(FileProgress::new_error(
|
||||
file_url,
|
||||
upload_file.file_id.clone(),
|
||||
err.msg.clone(),
|
||||
)) {
|
||||
error!("[File] send global notifier failed: {}", err);
|
||||
}
|
||||
return Err(err);
|
||||
},
|
||||
}
|
||||
@@ -631,7 +651,6 @@ async fn start_upload(
|
||||
temp_storage,
|
||||
&upload_file,
|
||||
completed_parts,
|
||||
&progress_notifiers,
|
||||
&global_notifier,
|
||||
)
|
||||
.await;
|
||||
@@ -641,9 +660,10 @@ async fn start_upload(
|
||||
.payload(err.clone())
|
||||
.send();
|
||||
}
|
||||
|
||||
return Err(err);
|
||||
}
|
||||
|
||||
trace!("[File] {} upload completed", upload_file.file_id);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -653,7 +673,6 @@ async fn resume_upload(
|
||||
user_service: &Arc<dyn StorageUserService>,
|
||||
temp_storage: &Arc<FileTempStorage>,
|
||||
upload_file: UploadFileTable,
|
||||
progress_notifiers: Arc<DashMap<String, ProgressNotifier>>,
|
||||
global_notifier: GlobalNotifier,
|
||||
) -> FlowyResult<()> {
|
||||
trace!(
|
||||
@@ -673,26 +692,22 @@ async fn resume_upload(
|
||||
temp_storage,
|
||||
chunked_bytes,
|
||||
&upload_file,
|
||||
progress_notifiers,
|
||||
global_notifier,
|
||||
)
|
||||
.await?;
|
||||
},
|
||||
Err(err) => {
|
||||
//
|
||||
match err.kind() {
|
||||
ErrorKind::NotFound => {
|
||||
error!("[File] file not found: {}", upload_file.local_file_path);
|
||||
if let Ok(uid) = user_service.user_id() {
|
||||
if let Ok(conn) = user_service.sqlite_connection(uid) {
|
||||
delete_upload_file(conn, &upload_file.upload_id)?;
|
||||
}
|
||||
Err(err) => match err.kind() {
|
||||
ErrorKind::NotFound => {
|
||||
error!("[File] file not found: {}", upload_file.local_file_path);
|
||||
if let Ok(uid) = user_service.user_id() {
|
||||
if let Ok(conn) = user_service.sqlite_connection(uid) {
|
||||
delete_upload_file(conn, &upload_file.upload_id)?;
|
||||
}
|
||||
},
|
||||
_ => {
|
||||
error!("[File] read file failed: {}", err);
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
_ => {
|
||||
error!("[File] read file failed: {}", err);
|
||||
},
|
||||
},
|
||||
}
|
||||
Ok(())
|
||||
@@ -741,13 +756,21 @@ async fn complete_upload(
|
||||
temp_storage: &Arc<FileTempStorage>,
|
||||
upload_file: &UploadFileTable,
|
||||
parts: Vec<CompletedPartRequest>,
|
||||
progress_notifiers: &Arc<DashMap<String, ProgressNotifier>>,
|
||||
global_notifier: &GlobalNotifier,
|
||||
) -> Result<(), FlowyError> {
|
||||
trace!(
|
||||
"[File]: completing file upload: {}, num parts: {}",
|
||||
let file_url = cloud_service
|
||||
.get_object_url_v1(
|
||||
&upload_file.workspace_id,
|
||||
&upload_file.parent_dir,
|
||||
&upload_file.file_id,
|
||||
)
|
||||
.await?;
|
||||
|
||||
info!(
|
||||
"[File]: completing file upload: {}, num parts: {}, url:{}",
|
||||
upload_file.file_id,
|
||||
parts.len()
|
||||
parts.len(),
|
||||
file_url
|
||||
);
|
||||
match cloud_service
|
||||
.complete_upload(
|
||||
@@ -761,27 +784,12 @@ async fn complete_upload(
|
||||
{
|
||||
Ok(_) => {
|
||||
info!("[File] completed upload file: {}", upload_file.file_id);
|
||||
if let Some(mut notifier) = progress_notifiers.get_mut(&upload_file.file_id) {
|
||||
notifier
|
||||
.notify(FileUploadState::Finished {
|
||||
file_id: upload_file.file_id.clone(),
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
let file_url = cloud_service
|
||||
.get_object_url_v1(
|
||||
&upload_file.workspace_id,
|
||||
&upload_file.parent_dir,
|
||||
&upload_file.file_id,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let progress = FileProgress::new_progress(file_url, 1.0);
|
||||
let progress = FileProgress::new_progress(file_url, upload_file.file_id.clone(), 1.0);
|
||||
info!(
|
||||
"[File]: notify upload progress:{}, {}",
|
||||
upload_file.file_id, progress
|
||||
);
|
||||
|
||||
if let Err(err) = global_notifier.send(progress) {
|
||||
error!("[File] send global notifier failed: {}", err);
|
||||
}
|
||||
@@ -797,6 +805,18 @@ async fn complete_upload(
|
||||
},
|
||||
Err(err) => {
|
||||
error!("[File] complete upload failed: {}", err);
|
||||
|
||||
let progress =
|
||||
FileProgress::new_error(file_url, upload_file.file_id.clone(), err.msg.clone());
|
||||
if let Err(send_err) = global_notifier.send(progress) {
|
||||
error!("[File] send global notifier failed: {}", send_err);
|
||||
}
|
||||
|
||||
let conn = user_service.sqlite_connection(user_service.user_id()?)?;
|
||||
if let Err(err) = delete_all_upload_parts(conn, &upload_file.upload_id) {
|
||||
error!("[File] delete all upload parts failed: {}", err);
|
||||
}
|
||||
return Err(err);
|
||||
},
|
||||
}
|
||||
Ok(())
|
||||
|
||||
@@ -137,6 +137,14 @@ pub fn delete_upload_file(mut conn: DBConnection, upload_id: &str) -> FlowyResul
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn delete_all_upload_parts(mut conn: DBConnection, upload_id: &str) -> FlowyResult<()> {
|
||||
diesel::delete(
|
||||
upload_file_part::dsl::upload_file_part.filter(upload_file_part::upload_id.eq(upload_id)),
|
||||
)
|
||||
.execute(&mut *conn)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn insert_upload_part(
|
||||
mut conn: DBConnection,
|
||||
upload_part: &UploadFilePartTable,
|
||||
@@ -172,11 +180,14 @@ pub fn select_upload_parts(
|
||||
pub fn batch_select_upload_file(
|
||||
mut conn: DBConnection,
|
||||
limit: i32,
|
||||
is_finish: bool,
|
||||
) -> FlowyResult<Vec<UploadFileTable>> {
|
||||
let results = upload_file_table::dsl::upload_file_table
|
||||
.filter(upload_file_table::is_finish.eq(is_finish))
|
||||
.order(upload_file_table::created_at.desc())
|
||||
.limit(limit.into())
|
||||
.load::<UploadFileTable>(&mut conn)?;
|
||||
.load::<UploadFileTable>(&mut *conn)?;
|
||||
|
||||
Ok(results)
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ use std::sync::atomic::{AtomicBool, AtomicU8};
|
||||
use std::sync::{Arc, Weak};
|
||||
use std::time::Duration;
|
||||
use tokio::sync::{watch, RwLock};
|
||||
use tracing::{error, info, trace};
|
||||
use tracing::{error, info, instrument, trace, warn};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum Signal {
|
||||
@@ -34,7 +34,7 @@ impl UploadTaskQueue {
|
||||
pub async fn queue_task(&self, task: UploadTask) {
|
||||
trace!("[File] Queued task: {}", task);
|
||||
self.tasks.write().await.push(task);
|
||||
let _ = self.notifier.send(Signal::Proceed);
|
||||
let _ = self.notifier.send_replace(Signal::Proceed);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,6 +104,7 @@ impl FileUploader {
|
||||
let _ = self.queue.notifier.send(Signal::ProceedAfterSecs(3));
|
||||
}
|
||||
|
||||
#[instrument(name = "[File]: process next", level = "debug", skip(self))]
|
||||
pub async fn process_next(&self) -> Option<()> {
|
||||
// Do not proceed if the uploader is paused.
|
||||
if self.pause_sync.load(std::sync::atomic::Ordering::Relaxed) {
|
||||
@@ -125,6 +126,7 @@ impl FileUploader {
|
||||
{
|
||||
// If the current uploads count is greater than or equal to the max uploads, do not proceed.
|
||||
let _ = self.queue.notifier.send(Signal::ProceedAfterSecs(10));
|
||||
trace!("[File] max uploads reached, process_next after 10 seconds");
|
||||
return None;
|
||||
}
|
||||
|
||||
@@ -133,6 +135,7 @@ impl FileUploader {
|
||||
.load(std::sync::atomic::Ordering::SeqCst)
|
||||
{
|
||||
// If the storage limitation is enabled, do not proceed.
|
||||
error!("[File] storage limit exceeded, uploader is disabled");
|
||||
return None;
|
||||
}
|
||||
|
||||
@@ -140,6 +143,7 @@ impl FileUploader {
|
||||
if task.retry_count() > 5 {
|
||||
// If the task has been retried more than 5 times, we should not retry it anymore.
|
||||
let _ = self.queue.notifier.send(Signal::ProceedAfterSecs(2));
|
||||
warn!("[File] Task has been retried more than 5 times: {}", task);
|
||||
return None;
|
||||
}
|
||||
|
||||
@@ -166,12 +170,12 @@ impl FileUploader {
|
||||
.await
|
||||
{
|
||||
if err.is_file_limit_exceeded() {
|
||||
error!("Failed to upload file: {}", err);
|
||||
error!("[File] Failed to upload file: {}", err);
|
||||
self.disable_storage_write();
|
||||
}
|
||||
|
||||
info!(
|
||||
"Failed to upload file: {}, retry_count:{}",
|
||||
"[File] Failed to upload file: {}, retry_count:{}",
|
||||
err, retry_count
|
||||
);
|
||||
|
||||
@@ -197,12 +201,12 @@ impl FileUploader {
|
||||
.await
|
||||
{
|
||||
if err.is_file_limit_exceeded() {
|
||||
error!("Failed to upload file: {}", err);
|
||||
error!("[File] failed to upload file: {}", err);
|
||||
self.disable_storage_write();
|
||||
}
|
||||
|
||||
info!(
|
||||
"Failed to resume upload file: {}, retry_count:{}",
|
||||
"[File] failed to resume upload file: {}, retry_count:{}",
|
||||
err, retry_count
|
||||
);
|
||||
retry_count += 1;
|
||||
@@ -216,10 +220,15 @@ impl FileUploader {
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
self
|
||||
.current_uploads
|
||||
.fetch_sub(1, std::sync::atomic::Ordering::SeqCst);
|
||||
let _ = self.queue.notifier.send(Signal::ProceedAfterSecs(2));
|
||||
trace!("[File] process_next after 2 seconds");
|
||||
self
|
||||
.queue
|
||||
.notifier
|
||||
.send_replace(Signal::ProceedAfterSecs(2));
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ async fn test_insert_new_upload() {
|
||||
|
||||
// select
|
||||
let conn = db.get_connection().unwrap();
|
||||
let records = batch_select_upload_file(conn, 100).unwrap();
|
||||
let records = batch_select_upload_file(conn, 100, false).unwrap();
|
||||
|
||||
assert_eq!(records.len(), 5);
|
||||
// compare the upload id order is the same as upload_ids
|
||||
@@ -55,7 +55,7 @@ async fn test_insert_new_upload() {
|
||||
}
|
||||
|
||||
let conn = db.get_connection().unwrap();
|
||||
let records = batch_select_upload_file(conn, 100).unwrap();
|
||||
let records = batch_select_upload_file(conn, 100, false).unwrap();
|
||||
assert!(records.is_empty());
|
||||
}
|
||||
|
||||
|
||||
@@ -73,8 +73,16 @@ impl UserManager {
|
||||
let reminder = Reminder::from(reminder_pb);
|
||||
self
|
||||
.mut_awareness(|user_awareness| {
|
||||
user_awareness.update_reminder(&reminder.id, |new_reminder| {
|
||||
new_reminder.clone_from(&reminder)
|
||||
user_awareness.update_reminder(&reminder.id, |update| {
|
||||
update
|
||||
.set_object_id(&reminder.object_id)
|
||||
.set_title(&reminder.title)
|
||||
.set_message(&reminder.message)
|
||||
.set_is_ack(reminder.is_ack)
|
||||
.set_is_read(reminder.is_read)
|
||||
.set_scheduled_at(reminder.scheduled_at)
|
||||
.set_type(reminder.ty)
|
||||
.set_meta(reminder.meta.clone().into_inner());
|
||||
});
|
||||
})
|
||||
.await?;
|
||||
|
||||
Reference in new issue
Block a user