Compare commits
32
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
71d1189048 | ||
|
|
757009a97d | ||
|
|
e8665dc76c | ||
|
|
f022f9aa06 | ||
|
|
32650b7941 | ||
|
|
5a7339b092 | ||
|
|
7ca028942c | ||
|
|
6fbd88fe76 | ||
|
|
2b5ecf5609 | ||
|
|
18672b197c | ||
|
|
63a267ca6d | ||
|
|
1f1a97b21b | ||
|
|
ca1ce8e19e | ||
|
|
f8f0599462 | ||
|
|
a4845e668d | ||
|
|
dd5bc7808c | ||
|
|
a993a2a181 | ||
|
|
99ad9dcaff | ||
|
|
d44de62b90 | ||
|
|
33b5837f04 | ||
|
|
54f757d9b2 | ||
|
|
cf27409c8d | ||
|
|
bd5250ae2f | ||
|
|
f0a4b4b77d | ||
|
|
151ee89855 | ||
|
|
2b7282195b | ||
|
|
4a1714b537 | ||
|
|
6731037a8c | ||
|
|
06f056aa4b | ||
|
|
a2e9d63f63 | ||
|
|
5d0a349236 | ||
|
|
db8d030a85 |
No files matched your search
@@ -138,7 +138,7 @@ jobs:
|
||||
job:
|
||||
- {
|
||||
target: x86_64-apple-darwin,
|
||||
os: macos-10.15,
|
||||
os: macos-11,
|
||||
extra-build-args: "",
|
||||
}
|
||||
steps:
|
||||
|
||||
@@ -1,5 +1,32 @@
|
||||
# Release Notes
|
||||
|
||||
## Version 0.1.6 - 30/05/2023
|
||||
|
||||
### New features
|
||||
- Support exporting your document and database data.
|
||||
|
||||
## Version 0.1.5 - 11/05/2023
|
||||
|
||||
### Bug Fixes
|
||||
- Fix: calendar dates don't match with weekdays.
|
||||
- Fix: sort numbers in Grid.
|
||||
|
||||
## Version 0.1.4 - 04/05/2023
|
||||
|
||||
### New features
|
||||
- Use AppFlowy’s calendar views to plan and manage tasks and deadlines.
|
||||
- Writing can be improved with the help of OpenAI.
|
||||
|
||||
## Version 0.1.3 - 24/04/2023
|
||||
|
||||
### New features
|
||||
- Launch the official Dark Mode.
|
||||
- Customize the font color and highlight color by setting a hex color value and an opacity level.
|
||||
|
||||
### Bug Fixes
|
||||
- Fix: the slash menu can be triggered by all other keyboards than English.
|
||||
- Fix: convert the single asterisk to italic text and the double asterisks to bold text.
|
||||
|
||||
## Version 0.1.2 - 03/28/2023
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -23,9 +23,11 @@ You are in charge of your data and customizations.
|
||||
<a href="https://twitter.com/appflowy"><b>Twitter</b></a>
|
||||
</p>
|
||||
|
||||
<p align="center"><img src="https://user-images.githubusercontent.com/12026239/200787830-96be260b-d0a0-4152-864e-6730b19095cd.png" alt="The Open Source Alternative To Notion." width="1000px" /></p>
|
||||
<p align="center"><img src="https://user-images.githubusercontent.com/12026239/174753177-98e4c899-2356-4137-bb42-374bba2b127b.png" alt="The Open Source Alternative To Notion." width="1000px" /></p>
|
||||
<p align="center"><img src="https://user-images.githubusercontent.com/12026239/190650183-a940f1e0-a2c5-4797-ab3a-56758f6f696c.png" alt="The Open Source Alternative To Notion." width="1000px" /></p>
|
||||
<p align="center"><img src="https://user-images.githubusercontent.com/12026239/236664610-fc209a97-815e-4716-af07-d94a859d1907.png" alt="AppFlowy Docs & Notes & Wikis" width="1000px" /></p>
|
||||
<p align="center"><img src="https://user-images.githubusercontent.com/12026239/236664628-5def2450-914a-4b2d-b907-92b7476b9863.png" alt="AppFlowy Databases for Tasks and Projects" width="1000px" /></p>
|
||||
<p align="center"><img src="https://user-images.githubusercontent.com/12026239/236664642-22e26c1b-5eae-4635-9aa6-b12ecf1c3c46.png" alt="AppFlowy Kanban Board for To-Dos" width="1000px" /></p>
|
||||
<p align="center"><img src="https://github.com/AppFlowy-IO/AppFlowy/assets/12026239/6be93d2b-a5c5-48a9-b7cf-c599d5f5140c" alt="AppFlowy Calendars for Plan and Manage Content" width="1000px" /></p>
|
||||
<p align="center"><img src="https://user-images.githubusercontent.com/12026239/236664657-dc5291f3-67b0-4a43-a818-640e92735deb.png" alt="AppFlowy OpenAI GPT Writers" width="1000px" /></p>
|
||||
|
||||
## User Installation
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true
|
||||
CARGO_MAKE_CRATE_FS_NAME = "dart_ffi"
|
||||
CARGO_MAKE_CRATE_NAME = "dart-ffi"
|
||||
LIB_NAME = "dart_ffi"
|
||||
CURRENT_APP_VERSION = "0.1.3"
|
||||
CURRENT_APP_VERSION = "0.1.6"
|
||||
FLUTTER_DESKTOP_FEATURES = "dart,rev-sqlite"
|
||||
PRODUCT_NAME = "AppFlowy"
|
||||
# CRATE_TYPE: https://doc.rust-lang.org/reference/linkage.html
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M7 8.91521C7.21574 9.22582 7.49099 9.48283 7.80707 9.66881C8.12315 9.85479 8.47268 9.96538 8.83194 9.99309C9.1912 10.0208 9.5518 9.96497 9.88926 9.8294C10.2267 9.69383 10.5332 9.48169 10.7878 9.20736L12.2949 7.58431C12.7525 7.07413 13.0056 6.39083 12.9999 5.68156C12.9942 4.9723 12.73 4.29384 12.2643 3.7923C11.7986 3.29075 11.1686 3.00627 10.51 3.0001C9.85142 2.99394 9.21693 3.26659 8.7432 3.75935L7.87913 4.68448" stroke="#333333" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M9 7.08479C8.78426 6.77418 8.50901 6.51717 8.19293 6.33119C7.87685 6.14521 7.52732 6.03462 7.16806 6.00691C6.8088 5.9792 6.4482 6.03503 6.11074 6.1706C5.77327 6.30617 5.46683 6.51831 5.21218 6.79264L3.7051 8.41569C3.24755 8.92587 2.99437 9.60918 3.00009 10.3184C3.00582 11.0277 3.26998 11.7062 3.73569 12.2077C4.2014 12.7092 4.8314 12.9937 5.48999 12.9999C6.14858 13.0061 6.78307 12.7334 7.2568 12.2407L8.11584 11.3155" stroke="#333333" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -45,7 +45,8 @@
|
||||
"medium": "medium",
|
||||
"large": "large",
|
||||
"fontSize": "Font Size",
|
||||
"import": "Import"
|
||||
"import": "Import",
|
||||
"moreOptions": "More options"
|
||||
},
|
||||
"disclosureAction": {
|
||||
"rename": "Rename",
|
||||
@@ -183,6 +184,7 @@
|
||||
},
|
||||
"files": {
|
||||
"defaultLocation": "Where your data is stored now",
|
||||
"exportData": "Export your data",
|
||||
"doubleTapToCopy": "Double tap to copy the path",
|
||||
"restoreLocation": "Restore to AppFlowy default path",
|
||||
"customizeLocation": "Open another folder",
|
||||
@@ -201,7 +203,9 @@
|
||||
"create": "Create",
|
||||
"folderPath": "Path to store your folder",
|
||||
"locationCannotBeEmpty": "Path cannot be empty",
|
||||
"pathCopiedSnackbar": "File storage path copied to clipboard!"
|
||||
"pathCopiedSnackbar": "File storage path copied to clipboard!",
|
||||
"exportFileSuccess": "Export file successfully!",
|
||||
"exportFileFail": "Export file failed!"
|
||||
},
|
||||
"user": {
|
||||
"name": "Name",
|
||||
@@ -363,7 +367,8 @@
|
||||
"smartEditFixSpelling": "Fix spelling",
|
||||
"warning": "⚠️ AI responses can be inaccurate or misleading.",
|
||||
"smartEditSummarize": "Summarize",
|
||||
"smartEditImproveWriting": "Improve Writing",
|
||||
"smartEditImproveWriting": "Improve writing",
|
||||
"smartEditMakeLonger": "Make longer",
|
||||
"smartEditCouldNotFetchResult": "Could not fetch result from OpenAI",
|
||||
"smartEditCouldNotFetchKey": "Could not fetch OpenAI key",
|
||||
"smartEditDisabled": "Connect OpenAI in Settings",
|
||||
@@ -418,7 +423,9 @@
|
||||
"showWeekNumbers": "Show week numbers",
|
||||
"showWeekends": "Show weekends",
|
||||
"firstDayOfWeek": "Start week on",
|
||||
"layoutDateField": "Layout calendar by"
|
||||
"layoutDateField": "Layout calendar by",
|
||||
"noDateTitle": "No Date",
|
||||
"emptyNoDate": "No unscheduled events"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -34,6 +34,8 @@ void main() {
|
||||
group('board', () {
|
||||
setUpAll(() async => await service.setUpAll());
|
||||
setUp(() async => await service.setUp());
|
||||
tearDown(() async => await service.tearDown());
|
||||
tearDownAll(() async => await service.tearDownAll());
|
||||
|
||||
testWidgets(
|
||||
'integration test unzips the proper workspace and loads it correctly.',
|
||||
|
||||
@@ -30,6 +30,8 @@ void main() {
|
||||
group('Tests on a workspace with only an empty document', () {
|
||||
setUpAll(() async => await service.setUpAll());
|
||||
setUp(() async => await service.setUp());
|
||||
tearDown(() async => await service.tearDown());
|
||||
tearDownAll(() async => await service.tearDownAll());
|
||||
|
||||
testWidgets('/board shortcut creates a new board and view of the board',
|
||||
(tester) async {
|
||||
@@ -69,6 +71,7 @@ void main() {
|
||||
],
|
||||
tester: tester,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Checks whether new board is referenced and properly on the page.
|
||||
expect(find.byType(BuiltInPageWidget), findsOneWidget);
|
||||
@@ -113,7 +116,12 @@ void main() {
|
||||
expect(find.byType(SelectionMenuItemWidget), findsAtLeastNWidgets(2));
|
||||
|
||||
// Finalizes the slash command that creates the board.
|
||||
await simulateKeyDownEvent(LogicalKeyboardKey.enter);
|
||||
await FlowyTestKeyboard.simulateKeyDownEvent(
|
||||
[
|
||||
LogicalKeyboardKey.enter,
|
||||
],
|
||||
tester: tester,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Checks whether new board is referenced and properly on the page.
|
||||
|
||||
@@ -16,6 +16,8 @@ void main() {
|
||||
group('integration tests for open-ai smart menu', () {
|
||||
setUpAll(() async => await service.setUpAll());
|
||||
setUp(() async => await service.setUp());
|
||||
tearDown(() async => await service.tearDown());
|
||||
tearDownAll(() async => await service.tearDownAll());
|
||||
|
||||
testWidgets('testing selection on open-ai smart menu replace', (tester) async {
|
||||
final appFlowyEditor = await setUpOpenAITesting(tester);
|
||||
|
||||
@@ -39,6 +39,13 @@ enum TestWorkspace {
|
||||
return root;
|
||||
}
|
||||
|
||||
Future<void> clean() async {
|
||||
final Directory workspaceRoot = await root;
|
||||
if (await workspaceRoot.exists()) {
|
||||
workspaceRoot.delete(recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
String get _asset => 'assets/test/workspaces/$_name.zip';
|
||||
}
|
||||
|
||||
@@ -67,4 +74,12 @@ class TestWorkspaceService {
|
||||
await TestWorkspace._parent.then((value) => value.path),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> tearDown() async {
|
||||
await workspace.clean();
|
||||
}
|
||||
|
||||
Future<void> tearDownAll() async {
|
||||
await SharedPreferences.getInstance().then((value) => value.remove(kSettingsLocationDefaultLocation));
|
||||
}
|
||||
}
|
||||
+1
-6
@@ -45,12 +45,7 @@ class DateCellDataPersistence implements CellDataPersistence<DateCellData> {
|
||||
Future<Option<FlowyError>> save(DateCellData data) {
|
||||
var payload = DateChangesetPB.create()..cellPath = _makeCellPath(cellId);
|
||||
|
||||
// This is a bit of a hack. This converts the data.date which is in
|
||||
// UTC to Local but actually changes the timestamp instead of just
|
||||
// changing the isUtc flag
|
||||
final dateTime = DateTime(data.date.year, data.date.month, data.date.day);
|
||||
|
||||
final date = (dateTime.millisecondsSinceEpoch ~/ 1000).toString();
|
||||
final date = (data.date.millisecondsSinceEpoch ~/ 1000).toString();
|
||||
payload.date = date;
|
||||
payload.isUtc = data.date.isUtc;
|
||||
payload.includeTime = data.includeTime;
|
||||
|
||||
+1
@@ -51,5 +51,6 @@ class CalendarSettingEvent with _$CalendarSettingEvent {
|
||||
}
|
||||
|
||||
enum CalendarSettingAction {
|
||||
properties,
|
||||
layout,
|
||||
}
|
||||
+28
-41
@@ -1,24 +1,23 @@
|
||||
import 'package:appflowy/plugins/database_view/application/row/row_cache.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/row/row_data_controller.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/card/card.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/card/card_cell_builder.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/card/cells/card_cell.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/card/cells/number_card_cell.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/card/cells/url_card_cell.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/row/cell_builder.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/row/row_detail.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database/field_entities.pbenum.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/image.dart';
|
||||
import 'package:flowy_infra/size.dart';
|
||||
import 'package:flowy_infra/theme_extension.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/hover.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../../grid/presentation/layout/sizes.dart';
|
||||
import '../../widgets/row/cells/select_option_cell/extension.dart';
|
||||
import '../application/calendar_bloc.dart';
|
||||
import 'calendar_page.dart';
|
||||
|
||||
class CalendarDayCard extends StatelessWidget {
|
||||
final String viewId;
|
||||
@@ -102,7 +101,7 @@ class CalendarDayCard extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
GestureDetector _buildCard(BuildContext context, CalendarDayEvent event) {
|
||||
Widget _buildCard(BuildContext context, CalendarDayEvent event) {
|
||||
final styles = <FieldType, CardCellStyle>{
|
||||
FieldType.Number: NumberCardCellStyle(10),
|
||||
FieldType.URL: URLCardCellStyle(10),
|
||||
@@ -162,6 +161,9 @@ class CalendarDayCard extends StatelessWidget {
|
||||
});
|
||||
|
||||
renderHook.addSelectOptionHook((selectedOptions, cardData, _) {
|
||||
if (selectedOptions.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
final children = selectedOptions.map(
|
||||
(option) {
|
||||
return SelectOptionTag.fromOption(
|
||||
@@ -193,7 +195,12 @@ class CalendarDayCard extends StatelessWidget {
|
||||
cardData: event.dateFieldId,
|
||||
isEditing: false,
|
||||
cellBuilder: cellBuilder,
|
||||
openCard: (context) => _showRowDetailPage(event, context),
|
||||
openCard: (context) => showEventDetails(
|
||||
context: context,
|
||||
event: event,
|
||||
viewId: viewId,
|
||||
rowCache: _rowCache,
|
||||
),
|
||||
styleConfiguration: const RowCardStyleConfiguration(
|
||||
showAccessory: false,
|
||||
cellPadding: EdgeInsets.zero,
|
||||
@@ -203,44 +210,24 @@ class CalendarDayCard extends StatelessWidget {
|
||||
onEndEditing: () {},
|
||||
);
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () => _showRowDetailPage(event, context),
|
||||
child: MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 2),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.fromBorderSide(
|
||||
BorderSide(
|
||||
color: Theme.of(context).dividerColor,
|
||||
width: 1.5,
|
||||
),
|
||||
),
|
||||
borderRadius: Corners.s6Border,
|
||||
),
|
||||
child: card,
|
||||
),
|
||||
return FlowyHover(
|
||||
style: HoverStyle(
|
||||
hoverColor: Theme.of(context).colorScheme.tertiaryContainer,
|
||||
foregroundColorOnHover: Theme.of(context).colorScheme.onBackground,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showRowDetailPage(CalendarDayEvent event, BuildContext context) {
|
||||
final dataController = RowController(
|
||||
rowId: event.eventId,
|
||||
viewId: viewId,
|
||||
rowCache: _rowCache,
|
||||
);
|
||||
|
||||
FlowyOverlay.show(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return RowDetailPage(
|
||||
cellBuilder: GridCellBuilder(
|
||||
cellCache: _rowCache.cellCache,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 2),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.fromBorderSide(
|
||||
BorderSide(
|
||||
color: Theme.of(context).dividerColor,
|
||||
width: 1.5,
|
||||
),
|
||||
),
|
||||
dataController: dataController,
|
||||
);
|
||||
},
|
||||
borderRadius: Corners.s6Border,
|
||||
),
|
||||
child: card,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+36
-24
@@ -9,6 +9,7 @@ import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import '../../application/row/row_cache.dart';
|
||||
import '../../application/row/row_data_controller.dart';
|
||||
import '../../widgets/row/cell_builder.dart';
|
||||
import '../../widgets/row/row_detail.dart';
|
||||
@@ -76,7 +77,12 @@ class _CalendarPageState extends State<CalendarPage> {
|
||||
listenWhen: (p, c) => p.editEvent != c.editEvent,
|
||||
listener: (context, state) {
|
||||
if (state.editEvent != null) {
|
||||
_showEditEventPage(state.editEvent!.event!, context);
|
||||
showEventDetails(
|
||||
context: context,
|
||||
event: state.editEvent!.event!,
|
||||
viewId: widget.view.id,
|
||||
rowCache: _calendarBloc.rowCache,
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
@@ -165,8 +171,9 @@ class _CalendarPageState extends State<CalendarPage> {
|
||||
}
|
||||
|
||||
Widget _headerWeekDayBuilder(day) {
|
||||
// incoming day starts from Monday, the symbols start from Sunday
|
||||
final symbols = DateFormat.EEEE(context.locale.toLanguageTag()).dateSymbols;
|
||||
final weekDayString = symbols.WEEKDAYS[day];
|
||||
final weekDayString = symbols.WEEKDAYS[(day + 1) % 7];
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: CalendarSize.daysOfWeekInsets,
|
||||
@@ -210,27 +217,32 @@ class _CalendarPageState extends State<CalendarPage> {
|
||||
}
|
||||
|
||||
WeekDays _weekdayFromInt(int dayOfWeek) {
|
||||
// MonthView places the first day of week on the second column for some reason.
|
||||
return WeekDays.values[(dayOfWeek + 1) % 7];
|
||||
}
|
||||
|
||||
void _showEditEventPage(CalendarDayEvent event, BuildContext context) {
|
||||
final dataController = RowController(
|
||||
rowId: event.eventId,
|
||||
viewId: widget.view.id,
|
||||
rowCache: _calendarBloc.rowCache,
|
||||
);
|
||||
|
||||
FlowyOverlay.show(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return RowDetailPage(
|
||||
cellBuilder: GridCellBuilder(
|
||||
cellCache: _calendarBloc.rowCache.cellCache,
|
||||
),
|
||||
dataController: dataController,
|
||||
);
|
||||
},
|
||||
);
|
||||
// dayOfWeek starts from Sunday, WeekDays starts from Monday
|
||||
return WeekDays.values[(dayOfWeek - 1) % 7];
|
||||
}
|
||||
}
|
||||
|
||||
void showEventDetails({
|
||||
required BuildContext context,
|
||||
required CalendarDayEvent event,
|
||||
required String viewId,
|
||||
required RowCache rowCache,
|
||||
}) {
|
||||
final dataController = RowController(
|
||||
rowId: event.eventId,
|
||||
viewId: viewId,
|
||||
rowCache: rowCache,
|
||||
);
|
||||
|
||||
FlowyOverlay.show(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return RowDetailPage(
|
||||
cellBuilder: GridCellBuilder(
|
||||
cellCache: rowCache.cellCache,
|
||||
),
|
||||
dataController: dataController,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
+8
-5
@@ -217,6 +217,7 @@ class LayoutDateField extends StatelessWidget {
|
||||
direction: PopoverDirection.leftWithTopAligned,
|
||||
constraints: BoxConstraints.loose(const Size(300, 400)),
|
||||
mutex: popoverMutex,
|
||||
offset: const Offset(-16, 0),
|
||||
popupBuilder: (context) {
|
||||
return BlocProvider(
|
||||
create: (context) => getIt<DatabasePropertyBloc>(
|
||||
@@ -237,9 +238,9 @@ class LayoutDateField extends StatelessWidget {
|
||||
onUpdated(fieldInfo.id);
|
||||
popoverMutex.close();
|
||||
},
|
||||
leftIcon: svgWidget('grid/field/date'),
|
||||
leftIcon: const FlowySvg(name: 'grid/field/date'),
|
||||
rightIcon: fieldInfo.id == fieldId
|
||||
? svgWidget('grid/checkmark')
|
||||
? const FlowySvg(name: 'grid/checkmark')
|
||||
: null,
|
||||
),
|
||||
);
|
||||
@@ -333,12 +334,13 @@ class FirstDayOfWeek extends StatelessWidget {
|
||||
direction: PopoverDirection.leftWithTopAligned,
|
||||
constraints: BoxConstraints.loose(const Size(300, 400)),
|
||||
mutex: popoverMutex,
|
||||
offset: const Offset(-16, 0),
|
||||
popupBuilder: (context) {
|
||||
final symbols =
|
||||
DateFormat.EEEE(context.locale.toLanguageTag()).dateSymbols;
|
||||
// starts from sunday
|
||||
final items = symbols.WEEKDAYS.asMap().entries.map((entry) {
|
||||
final index = (entry.key - 1) % 7;
|
||||
final index = entry.key;
|
||||
final string = entry.value;
|
||||
return SizedBox(
|
||||
height: GridSize.popoverItemHeight,
|
||||
@@ -348,8 +350,9 @@ class FirstDayOfWeek extends StatelessWidget {
|
||||
onUpdated(index);
|
||||
popoverMutex.close();
|
||||
},
|
||||
rightIcon:
|
||||
firstDayOfWeek == index ? svgWidget('grid/checkmark') : null,
|
||||
rightIcon: firstDayOfWeek == index
|
||||
? const FlowySvg(name: 'grid/checkmark')
|
||||
: null,
|
||||
),
|
||||
);
|
||||
}).toList();
|
||||
|
||||
+25
@@ -2,8 +2,10 @@ import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/field/field_controller.dart';
|
||||
import 'package:appflowy/plugins/database_view/calendar/application/calendar_setting_bloc.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/toolbar/grid_property.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database/protobuf.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/image.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/button.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/scrolling/styled_list.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||
@@ -38,6 +40,11 @@ class CalendarSetting extends StatelessWidget {
|
||||
final CalendarSettingAction? action =
|
||||
state.selectedAction.foldLeft(null, (previous, action) => action);
|
||||
switch (action) {
|
||||
case CalendarSettingAction.properties:
|
||||
return GridPropertyList(
|
||||
viewId: settingContext.viewId,
|
||||
fieldController: settingContext.fieldController,
|
||||
);
|
||||
case CalendarSettingAction.layout:
|
||||
return CalendarLayoutSetting(
|
||||
onUpdated: onUpdated,
|
||||
@@ -78,9 +85,16 @@ class AllCalendarSettings extends StatelessWidget {
|
||||
}
|
||||
|
||||
Widget _settingItem(BuildContext context, CalendarSettingAction action) {
|
||||
Widget? icon;
|
||||
if (action.iconName() != null) {
|
||||
icon = FlowySvg(
|
||||
name: action.iconName()!,
|
||||
);
|
||||
}
|
||||
return SizedBox(
|
||||
height: GridSize.popoverItemHeight,
|
||||
child: FlowyButton(
|
||||
leftIcon: icon,
|
||||
text: FlowyText.medium(action.title()),
|
||||
onTap: () {
|
||||
context
|
||||
@@ -93,8 +107,19 @@ class AllCalendarSettings extends StatelessWidget {
|
||||
}
|
||||
|
||||
extension _SettingExtension on CalendarSettingAction {
|
||||
String? iconName() {
|
||||
switch (this) {
|
||||
case CalendarSettingAction.properties:
|
||||
return 'grid/setting/properties';
|
||||
case CalendarSettingAction.layout:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
String title() {
|
||||
switch (this) {
|
||||
case CalendarSettingAction.properties:
|
||||
return LocaleKeys.grid_settings_Properties.tr();
|
||||
case CalendarSettingAction.layout:
|
||||
return LocaleKeys.grid_settings_layout.tr();
|
||||
}
|
||||
|
||||
+103
-7
@@ -1,6 +1,8 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/database_view/calendar/presentation/calendar_page.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:calendar_view/calendar_view.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/theme_extension.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
@@ -19,7 +21,8 @@ class CalendarToolbar extends StatelessWidget {
|
||||
height: 40,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
children: const [
|
||||
_UnscheduleEventsButton(),
|
||||
_SettingButton(),
|
||||
],
|
||||
),
|
||||
@@ -28,25 +31,22 @@ class CalendarToolbar extends StatelessWidget {
|
||||
}
|
||||
|
||||
class _SettingButton extends StatefulWidget {
|
||||
const _SettingButton({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _SettingButtonState();
|
||||
}
|
||||
|
||||
class _SettingButtonState extends State<_SettingButton> {
|
||||
late PopoverController popoverController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
popoverController = PopoverController();
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AppFlowyPopover(
|
||||
controller: popoverController,
|
||||
direction: PopoverDirection.bottomWithRightAligned,
|
||||
triggerActions: PopoverTriggerFlags.none,
|
||||
constraints: BoxConstraints.loose(const Size(300, 400)),
|
||||
margin: EdgeInsets.zero,
|
||||
child: FlowyTextButton(
|
||||
@@ -54,7 +54,6 @@ class _SettingButtonState extends State<_SettingButton> {
|
||||
fillColor: Colors.transparent,
|
||||
hoverColor: AFThemeExtension.of(context).lightGreyHover,
|
||||
padding: GridSize.typeOptionContentInsets,
|
||||
onPressed: () => popoverController.show(),
|
||||
),
|
||||
popupBuilder: (BuildContext popoverContext) {
|
||||
final bloc = context.watch<CalendarBloc>();
|
||||
@@ -81,3 +80,100 @@ class _SettingButtonState extends State<_SettingButton> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _UnscheduleEventsButton extends StatefulWidget {
|
||||
const _UnscheduleEventsButton({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<_UnscheduleEventsButton> createState() =>
|
||||
_UnscheduleEventsButtonState();
|
||||
}
|
||||
|
||||
class _UnscheduleEventsButtonState extends State<_UnscheduleEventsButton> {
|
||||
late final PopoverController _controller;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = PopoverController();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<CalendarBloc, CalendarState>(
|
||||
builder: (context, state) {
|
||||
final unscheduledEvents = state.allEvents
|
||||
.where((e) => e.date == DateTime.fromMillisecondsSinceEpoch(0))
|
||||
.toList();
|
||||
final viewId = context.read<CalendarBloc>().viewId;
|
||||
final rowCache = context.read<CalendarBloc>().rowCache;
|
||||
return AppFlowyPopover(
|
||||
direction: PopoverDirection.bottomWithCenterAligned,
|
||||
controller: _controller,
|
||||
offset: const Offset(0, 8),
|
||||
child: FlowyTextButton(
|
||||
"${LocaleKeys.calendar_settings_noDateTitle.tr()} (${unscheduledEvents.length})",
|
||||
fillColor: Colors.transparent,
|
||||
hoverColor: AFThemeExtension.of(context).lightGreyHover,
|
||||
padding: GridSize.typeOptionContentInsets,
|
||||
),
|
||||
popupBuilder: (context) {
|
||||
if (unscheduledEvents.isEmpty) {
|
||||
return SizedBox(
|
||||
height: GridSize.popoverItemHeight,
|
||||
child: Center(
|
||||
child: FlowyText.medium(
|
||||
LocaleKeys.calendar_settings_emptyNoDate.tr(),
|
||||
color: Theme.of(context).hintColor,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return ListView.separated(
|
||||
itemBuilder: (context, index) => _UnscheduledEventItem(
|
||||
event: unscheduledEvents[index],
|
||||
onPressed: () {
|
||||
showEventDetails(
|
||||
context: context,
|
||||
event: unscheduledEvents[index].event!,
|
||||
viewId: viewId,
|
||||
rowCache: rowCache,
|
||||
);
|
||||
_controller.close();
|
||||
},
|
||||
),
|
||||
itemCount: unscheduledEvents.length,
|
||||
separatorBuilder: (context, index) =>
|
||||
VSpace(GridSize.typeOptionSeparatorHeight),
|
||||
shrinkWrap: true,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _UnscheduledEventItem extends StatelessWidget {
|
||||
final CalendarEventData<CalendarDayEvent> event;
|
||||
final VoidCallback onPressed;
|
||||
const _UnscheduledEventItem({
|
||||
required this.event,
|
||||
required this.onPressed,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
height: GridSize.popoverItemHeight,
|
||||
child: FlowyTextButton(
|
||||
event.title,
|
||||
fillColor: Colors.transparent,
|
||||
hoverColor: AFThemeExtension.of(context).lightGreyHover,
|
||||
padding: GridSize.typeOptionContentInsets,
|
||||
onPressed: onPressed,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+1
@@ -43,6 +43,7 @@ class RowCardContainer extends StatelessWidget {
|
||||
}
|
||||
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () => openCard(context),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
|
||||
+29
-6
@@ -84,15 +84,23 @@ class DateCellCalendarBloc
|
||||
bool? includeTime,
|
||||
}) {
|
||||
final DateCellData newDateData = state.dateCellData.fold(
|
||||
() => DateCellData(
|
||||
date: date ?? DateTime.now(),
|
||||
time: time,
|
||||
includeTime: includeTime ?? false,
|
||||
),
|
||||
() {
|
||||
DateTime newDate = DateTime.now();
|
||||
if (date != null) {
|
||||
newDate = _utcToLocalAddTime(date);
|
||||
}
|
||||
return DateCellData(
|
||||
date: newDate,
|
||||
time: time,
|
||||
includeTime: includeTime ?? false,
|
||||
);
|
||||
},
|
||||
(dateData) {
|
||||
var newDateData = dateData;
|
||||
if (date != null && !isSameDay(newDateData.date, date)) {
|
||||
newDateData = newDateData.copyWith(date: date);
|
||||
newDateData = newDateData.copyWith(
|
||||
date: _utcToLocalAddTime(date),
|
||||
);
|
||||
}
|
||||
|
||||
if (newDateData.time != time) {
|
||||
@@ -151,6 +159,21 @@ class DateCellCalendarBloc
|
||||
);
|
||||
}
|
||||
|
||||
DateTime _utcToLocalAddTime(DateTime date) {
|
||||
final now = DateTime.now();
|
||||
// the incoming date is Utc. this trick converts it into Local
|
||||
// and add the current time, though
|
||||
// the time may be overwritten by explicitly provided time string
|
||||
return DateTime(
|
||||
date.year,
|
||||
date.month,
|
||||
date.day,
|
||||
now.hour,
|
||||
now.minute,
|
||||
now.second,
|
||||
);
|
||||
}
|
||||
|
||||
String timeFormatPrompt(FlowyError error) {
|
||||
String msg = "${LocaleKeys.grid_field_invalidTimeFormat.tr()}.";
|
||||
switch (state.dateTypeOptionPB.timeFormat) {
|
||||
|
||||
+86
-47
@@ -19,6 +19,8 @@ import 'url_cell_bloc.dart';
|
||||
|
||||
class GridURLCellStyle extends GridCellStyle {
|
||||
String? placeholder;
|
||||
TextStyle? textStyle;
|
||||
bool? autofocus;
|
||||
|
||||
List<GridURLCellAccessoryType> accessoryTypes;
|
||||
|
||||
@@ -29,8 +31,8 @@ class GridURLCellStyle extends GridCellStyle {
|
||||
}
|
||||
|
||||
enum GridURLCellAccessoryType {
|
||||
edit,
|
||||
copyURL,
|
||||
visitURL,
|
||||
}
|
||||
|
||||
class GridURLCell extends GridCellWidget {
|
||||
@@ -56,15 +58,14 @@ class GridURLCell extends GridCellWidget {
|
||||
GridCellAccessoryBuildContext buildContext,
|
||||
) {
|
||||
switch (ty) {
|
||||
case GridURLCellAccessoryType.edit:
|
||||
case GridURLCellAccessoryType.visitURL:
|
||||
final cellContext = cellControllerBuilder.build() as URLCellController;
|
||||
return GridCellAccessoryBuilder(
|
||||
builder: (Key key) => _EditURLAccessory(
|
||||
builder: (Key key) => _VisitURLAccessory(
|
||||
key: key,
|
||||
anchorContext: buildContext.anchorContext,
|
||||
cellControllerBuilder: cellControllerBuilder,
|
||||
cellContext: cellContext,
|
||||
),
|
||||
);
|
||||
|
||||
case GridURLCellAccessoryType.copyURL:
|
||||
final cellContext = cellControllerBuilder.build() as URLCellController;
|
||||
return GridCellAccessoryBuilder(
|
||||
@@ -89,11 +90,11 @@ class GridURLCell extends GridCellWidget {
|
||||
);
|
||||
}
|
||||
|
||||
// If the accessories is empty then the default accessory will be GridURLCellAccessoryType.edit
|
||||
// If the accessories is empty then the default accessory will be GridURLCellAccessoryType.visitURL
|
||||
if (accessories.isEmpty) {
|
||||
accessories.add(
|
||||
accessoryFromType(
|
||||
GridURLCellAccessoryType.edit,
|
||||
GridURLCellAccessoryType.visitURL,
|
||||
buildContext,
|
||||
),
|
||||
);
|
||||
@@ -106,6 +107,8 @@ class GridURLCell extends GridCellWidget {
|
||||
class _GridURLCellState extends GridCellState<GridURLCell> {
|
||||
final _popoverController = PopoverController();
|
||||
late URLCellBloc _cellBloc;
|
||||
late TextEditingController _controller;
|
||||
late FocusNode _focusNode;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -113,6 +116,8 @@ class _GridURLCellState extends GridCellState<GridURLCell> {
|
||||
widget.cellControllerBuilder.build() as URLCellController;
|
||||
_cellBloc = URLCellBloc(cellController: cellController);
|
||||
_cellBloc.add(const URLCellEvent.initial());
|
||||
_controller = TextEditingController(text: _cellBloc.state.content);
|
||||
_focusNode = FocusNode();
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@@ -122,60 +127,61 @@ class _GridURLCellState extends GridCellState<GridURLCell> {
|
||||
value: _cellBloc,
|
||||
child: BlocBuilder<URLCellBloc, URLCellState>(
|
||||
builder: (context, state) {
|
||||
final richText = Padding(
|
||||
padding: GridSize.cellContentInsets,
|
||||
child: FlowyText.medium(
|
||||
state.content,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
decoration: TextDecoration.underline,
|
||||
final urlEditor = Padding(
|
||||
padding: EdgeInsets.only(
|
||||
left: GridSize.cellContentInsets.left,
|
||||
right: GridSize.cellContentInsets.right,
|
||||
),
|
||||
);
|
||||
|
||||
return AppFlowyPopover(
|
||||
margin: EdgeInsets.zero,
|
||||
controller: _popoverController,
|
||||
constraints: BoxConstraints.loose(const Size(300, 160)),
|
||||
direction: PopoverDirection.bottomWithLeftAligned,
|
||||
triggerActions: PopoverTriggerFlags.none,
|
||||
offset: const Offset(0, 8),
|
||||
child: SizedBox.expand(
|
||||
child: GestureDetector(
|
||||
child: Align(alignment: Alignment.centerLeft, child: richText),
|
||||
onTap: () async {
|
||||
final url = context.read<URLCellBloc>().state.url;
|
||||
await _openUrlOrEdit(url);
|
||||
},
|
||||
child: TextField(
|
||||
controller: _controller,
|
||||
focusNode: _focusNode,
|
||||
maxLines: 1,
|
||||
style: (widget.cellStyle?.textStyle ??
|
||||
Theme.of(context).textTheme.bodyMedium)
|
||||
?.copyWith(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
decoration: TextDecoration.underline,
|
||||
),
|
||||
autofocus: false,
|
||||
onEditingComplete: focusChanged,
|
||||
onSubmitted: (value) => focusChanged(isUrlSubmitted: true),
|
||||
decoration: InputDecoration(
|
||||
contentPadding: EdgeInsets.only(
|
||||
top: GridSize.cellContentInsets.top,
|
||||
bottom: GridSize.cellContentInsets.bottom,
|
||||
),
|
||||
border: InputBorder.none,
|
||||
hintText: widget.cellStyle?.placeholder,
|
||||
isDense: true,
|
||||
),
|
||||
),
|
||||
popupBuilder: (BuildContext popoverContext) {
|
||||
return URLEditorPopover(
|
||||
cellController:
|
||||
widget.cellControllerBuilder.build() as URLCellController,
|
||||
onExit: () => _popoverController.close(),
|
||||
);
|
||||
},
|
||||
onClose: () {
|
||||
widget.onCellEditing.value = false;
|
||||
},
|
||||
);
|
||||
return urlEditor;
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void focusChanged({
|
||||
bool isUrlSubmitted = false,
|
||||
}) {
|
||||
if (mounted) {
|
||||
if (_cellBloc.isClosed == false &&
|
||||
_controller.text != _cellBloc.state.content) {
|
||||
_cellBloc.add(URLCellEvent.updateURL(_controller.text));
|
||||
}
|
||||
if (isUrlSubmitted) {
|
||||
_focusNode.unfocus();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> dispose() async {
|
||||
_cellBloc.close();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _openUrlOrEdit(String url) async {
|
||||
final uri = Uri.parse(url);
|
||||
if (url.isNotEmpty && await canLaunchUrl(uri)) {
|
||||
await launchUrl(uri);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void requestBeginFocus() {
|
||||
widget.onCellEditing.value = true;
|
||||
@@ -269,3 +275,36 @@ class _CopyURLAccessoryState extends State<_CopyURLAccessory>
|
||||
showMessageToast(LocaleKeys.grid_row_copyProperty.tr());
|
||||
}
|
||||
}
|
||||
|
||||
class _VisitURLAccessory extends StatefulWidget {
|
||||
final URLCellController cellContext;
|
||||
const _VisitURLAccessory({required this.cellContext, Key? key})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _VisitURLAccessoryState();
|
||||
}
|
||||
|
||||
class _VisitURLAccessoryState extends State<_VisitURLAccessory>
|
||||
with GridCellAccessoryState {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return svgWidget(
|
||||
"editor/link",
|
||||
color: AFThemeExtension.of(context).textColor,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void onTap() {
|
||||
var content =
|
||||
widget.cellContext.getCellData(loadIfNotExist: false)?.content ?? "";
|
||||
if (!content.contains('http://')) {
|
||||
content = 'http://$content';
|
||||
}
|
||||
final uri = Uri.parse(content);
|
||||
if (content.isNotEmpty) {
|
||||
canLaunchUrl(uri).then((value) => launchUrl(uri));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -361,8 +361,8 @@ GridCellStyle? _customCellStyle(FieldType fieldType) {
|
||||
return GridURLCellStyle(
|
||||
placeholder: LocaleKeys.grid_row_textPlaceholder.tr(),
|
||||
accessoryTypes: [
|
||||
GridURLCellAccessoryType.edit,
|
||||
GridURLCellAccessoryType.copyURL,
|
||||
GridURLCellAccessoryType.visitURL,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -226,6 +226,9 @@ class _AppFlowyEditorPageState extends State<_AppFlowyEditorPage> {
|
||||
if (temporaryNodeTypes.contains(node.type)) {
|
||||
transaction.deleteNode(node);
|
||||
}
|
||||
if (kCoverType == node.type && !node.path.equals([0])) {
|
||||
transaction.deleteNode(node);
|
||||
}
|
||||
}
|
||||
if (transaction.operations.isNotEmpty) {
|
||||
await editorState.apply(transaction, withUpdateCursor: false);
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/more/cubit/document_appearance_cubit.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/more/font_size_switcher.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
@@ -14,6 +16,7 @@ class DocumentMoreButton extends StatelessWidget {
|
||||
return PopupMenuButton<int>(
|
||||
color: Theme.of(context).colorScheme.surfaceVariant,
|
||||
offset: const Offset(0, 30),
|
||||
tooltip: LocaleKeys.moreAction_moreOptions.tr(),
|
||||
itemBuilder: (context) {
|
||||
return [
|
||||
PopupMenuItem(
|
||||
|
||||
+45
-50
@@ -1,11 +1,10 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/emoji_picker/emoji_picker.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/emoji_picker/src/default_emoji_picker_view.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/emoji_picker/src/emoji_view_state.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart' hide FlowySvg;
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/button.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||
import 'package:flowy_infra/image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class EmojiPopover extends StatefulWidget {
|
||||
@@ -33,57 +32,53 @@ class _EmojiPopoverState extends State<EmojiPopover> {
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(15),
|
||||
child: EmojiPicker(
|
||||
onEmojiSelected: (category, emoji) {
|
||||
widget.onEmojiChanged(emoji);
|
||||
},
|
||||
customWidget: (Config config, EmojiViewState state) {
|
||||
return Stack(
|
||||
alignment: Alignment.topRight,
|
||||
children: [
|
||||
Container(
|
||||
padding: EdgeInsets.only(top: widget.showRemoveButton ? 25 : 0),
|
||||
child: DefaultEmojiPickerView(config, state),
|
||||
),
|
||||
_buildDeleteButtonIfNeed(),
|
||||
],
|
||||
);
|
||||
},
|
||||
config: Config(
|
||||
columns: 8,
|
||||
emojiSizeMax: 28,
|
||||
bgColor: Colors.transparent,
|
||||
iconColor: Theme.of(context).iconTheme.color!,
|
||||
iconColorSelected: Theme.of(context).colorScheme.onSurface,
|
||||
selectedHoverColor: Theme.of(context).colorScheme.secondary,
|
||||
progressIndicatorColor: Theme.of(context).iconTheme.color!,
|
||||
buttonMode: ButtonMode.CUPERTINO,
|
||||
initCategory: Category.RECENT,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDeleteButtonIfNeed() {
|
||||
if (!widget.showRemoveButton) {
|
||||
return const SizedBox();
|
||||
}
|
||||
return FlowyButton(
|
||||
onTap: () => widget.removeIcon(),
|
||||
useIntrinsicWidth: true,
|
||||
text: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
child: Column(
|
||||
children: [
|
||||
const FlowySvg(name: 'editor/delete'),
|
||||
const SizedBox(
|
||||
width: 5,
|
||||
),
|
||||
FlowyText(
|
||||
LocaleKeys.document_plugins_cover_removeIcon.tr(),
|
||||
if (widget.showRemoveButton)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 4.0),
|
||||
child: Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: DeleteButton(onPressed: widget.removeIcon),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: EmojiPicker(
|
||||
onEmojiSelected: (category, emoji) {
|
||||
widget.onEmojiChanged(emoji);
|
||||
},
|
||||
config: Config(
|
||||
columns: 8,
|
||||
emojiSizeMax: 28,
|
||||
bgColor: Colors.transparent,
|
||||
iconColor: Theme.of(context).iconTheme.color!,
|
||||
iconColorSelected: Theme.of(context).colorScheme.onSurface,
|
||||
selectedHoverColor: Theme.of(context).colorScheme.secondary,
|
||||
progressIndicatorColor: Theme.of(context).iconTheme.color!,
|
||||
buttonMode: ButtonMode.CUPERTINO,
|
||||
initCategory: Category.RECENT,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class DeleteButton extends StatelessWidget {
|
||||
final VoidCallback onPressed;
|
||||
const DeleteButton({required this.onPressed, Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FlowyButton(
|
||||
onTap: onPressed,
|
||||
useIntrinsicWidth: true,
|
||||
text: FlowyText(
|
||||
LocaleKeys.document_plugins_cover_removeIcon.tr(),
|
||||
),
|
||||
leftIcon: const FlowySvg(name: 'editor/delete'),
|
||||
);
|
||||
}
|
||||
}
|
||||
+23
-19
@@ -29,29 +29,32 @@ void _showEmojiSelectionMenu(
|
||||
menuService.dismiss();
|
||||
|
||||
_emojiSelectionMenu?.remove();
|
||||
_emojiSelectionMenu = OverlayEntry(builder: (context) {
|
||||
return Positioned(
|
||||
top: alignment == Alignment.bottomLeft ? offset.dy : null,
|
||||
bottom: alignment == Alignment.topLeft ? offset.dy : null,
|
||||
left: offset.dx,
|
||||
child: Material(
|
||||
child: EmojiSelectionMenu(
|
||||
editorState: editorState,
|
||||
onSubmitted: (text) {
|
||||
// insert emoji
|
||||
editorState.insertEmoji(text);
|
||||
},
|
||||
onExit: () {
|
||||
_dismissEmojiSelectionMenu();
|
||||
//close emoji panel
|
||||
},
|
||||
_emojiSelectionMenu = OverlayEntry(
|
||||
builder: (context) {
|
||||
return Positioned(
|
||||
top: alignment == Alignment.bottomLeft ? offset.dy : null,
|
||||
bottom: alignment == Alignment.topLeft ? offset.dy : null,
|
||||
left: offset.dx,
|
||||
child: Material(
|
||||
child: EmojiSelectionMenu(
|
||||
editorState: editorState,
|
||||
onSubmitted: (text) {
|
||||
// insert emoji
|
||||
editorState.insertEmoji(text);
|
||||
},
|
||||
onExit: () {
|
||||
_dismissEmojiSelectionMenu();
|
||||
//close emoji panel
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},);
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
Overlay.of(context).insert(_emojiSelectionMenu!);
|
||||
|
||||
_editorState = editorState;
|
||||
editorState.service.selectionService.currentSelection
|
||||
.addListener(_dismissEmojiSelectionMenu);
|
||||
}
|
||||
@@ -62,6 +65,7 @@ void _dismissEmojiSelectionMenu() {
|
||||
|
||||
_editorState?.service.selectionService.currentSelection
|
||||
.removeListener(_dismissEmojiSelectionMenu);
|
||||
_editorState?.service.keyboardService?.enable();
|
||||
_editorState = null;
|
||||
}
|
||||
|
||||
|
||||
+10
-1
@@ -6,7 +6,8 @@ import 'package:easy_localization/easy_localization.dart';
|
||||
enum SmartEditAction {
|
||||
summarize,
|
||||
fixSpelling,
|
||||
improveWriting;
|
||||
improveWriting,
|
||||
makeItLonger;
|
||||
|
||||
String get toInstruction {
|
||||
switch (this) {
|
||||
@@ -16,6 +17,8 @@ enum SmartEditAction {
|
||||
return 'Correct this to standard English:';
|
||||
case SmartEditAction.improveWriting:
|
||||
return 'Rewrite this in your own words:';
|
||||
case SmartEditAction.makeItLonger:
|
||||
return 'Make this text longer:';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +30,8 @@ enum SmartEditAction {
|
||||
return 'Correct this to standard English:\n\n$input';
|
||||
case SmartEditAction.improveWriting:
|
||||
return 'Rewrite this:\n\n$input';
|
||||
case SmartEditAction.makeItLonger:
|
||||
return 'Make this text longer:\n\n$input';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,6 +43,8 @@ enum SmartEditAction {
|
||||
return SmartEditAction.fixSpelling;
|
||||
case 2:
|
||||
return SmartEditAction.improveWriting;
|
||||
case 3:
|
||||
return SmartEditAction.makeItLonger;
|
||||
}
|
||||
return SmartEditAction.fixSpelling;
|
||||
}
|
||||
@@ -50,6 +57,8 @@ enum SmartEditAction {
|
||||
return LocaleKeys.document_plugins_smartEditFixSpelling.tr();
|
||||
case SmartEditAction.improveWriting:
|
||||
return LocaleKeys.document_plugins_smartEditImproveWriting.tr();
|
||||
case SmartEditAction.makeItLonger:
|
||||
return LocaleKeys.document_plugins_smartEditMakeLonger.tr();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,4 +179,4 @@ void _resolveGridDeps(GetIt getIt) {
|
||||
(viewId, cache) =>
|
||||
DatabasePropertyBloc(viewId: viewId, fieldController: cache),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import 'dart:ui';
|
||||
|
||||
class ColorGenerator {
|
||||
Color generateColorFromString(String string) {
|
||||
final hash = string.hashCode;
|
||||
int r = (hash & 0xFF0000) >> 16;
|
||||
int g = (hash & 0x00FF00) >> 8;
|
||||
int b = hash & 0x0000FF;
|
||||
return Color.fromRGBO(r, g, b, 0.5);
|
||||
}
|
||||
}
|
||||
+15
@@ -1,4 +1,5 @@
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/app.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class SettingsFileExportState {
|
||||
@@ -8,6 +9,20 @@ class SettingsFileExportState {
|
||||
initialize();
|
||||
}
|
||||
|
||||
List<ViewPB> get selectedViews {
|
||||
final selectedViews = <ViewPB>[];
|
||||
for (var i = 0; i < apps.length; i++) {
|
||||
if (selectedApps[i]) {
|
||||
for (var j = 0; j < apps[i].belongings.items.length; j++) {
|
||||
if (selectedItems[i][j]) {
|
||||
selectedViews.add(apps[i].belongings.items[j]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return selectedViews;
|
||||
}
|
||||
|
||||
List<AppPB> apps;
|
||||
List<bool> expanded = [];
|
||||
List<bool> selectedApps = [];
|
||||
|
||||
+4
@@ -61,6 +61,10 @@ class ViewSection extends StatelessWidget {
|
||||
.add(ViewSectionEvent.moveView(oldIndex, index));
|
||||
},
|
||||
ignorePrimaryScrollController: true,
|
||||
buildDraggableFeedback: (context, constraints, child) => ConstrainedBox(
|
||||
constraints: constraints,
|
||||
child: Material(color: Colors.transparent, child: child),
|
||||
),
|
||||
children: children,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -149,6 +149,8 @@ class HomeMenu extends StatelessWidget {
|
||||
),
|
||||
);
|
||||
},
|
||||
proxyDecorator: (child, index, animation) =>
|
||||
Material(color: Colors.transparent, child: child),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/util/color_generator/color_generator.dart';
|
||||
import 'package:appflowy/workspace/application/menu/menu_user_bloc.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/settings_dialog.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/settings_user_view.dart';
|
||||
@@ -45,8 +46,31 @@ class MenuUser extends StatelessWidget {
|
||||
String iconUrl = context.read<MenuUserBloc>().state.userProfile.iconUrl;
|
||||
if (iconUrl.isEmpty) {
|
||||
iconUrl = defaultUserAvatar;
|
||||
final String name = context.read<MenuUserBloc>().state.userProfile.name;
|
||||
final Color color = ColorGenerator().generateColorFromString(name);
|
||||
const initialsCount = 2;
|
||||
// Taking the first letters of the name components and limiting to 2 elements
|
||||
final nameInitials = name
|
||||
.split(' ')
|
||||
.where((element) => element.isNotEmpty)
|
||||
.take(initialsCount)
|
||||
.map((element) => element[0].toUpperCase())
|
||||
.join('');
|
||||
return Container(
|
||||
width: 28,
|
||||
height: 28,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: color,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: FlowyText.semibold(
|
||||
nameInitials,
|
||||
color: Colors.white,
|
||||
fontSize: nameInitials.length == initialsCount ? 12 : 14,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return SizedBox(
|
||||
width: 25,
|
||||
height: 25,
|
||||
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/settings_file_exporter_widget.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
|
||||
import '../../../../generated/locale_keys.g.dart';
|
||||
|
||||
class SettingsExportFileWidget extends StatefulWidget {
|
||||
const SettingsExportFileWidget({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
State<SettingsExportFileWidget> createState() =>
|
||||
SettingsExportFileWidgetState();
|
||||
}
|
||||
|
||||
@visibleForTesting
|
||||
class SettingsExportFileWidgetState extends State<SettingsExportFileWidget> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListTile(
|
||||
title: FlowyText.medium(
|
||||
LocaleKeys.settings_files_exportData.tr(),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Tooltip(
|
||||
message: LocaleKeys.settings_files_open.tr(),
|
||||
child: FlowyIconButton(
|
||||
height: 40,
|
||||
width: 40,
|
||||
icon: const Icon(Icons.folder_open_outlined),
|
||||
hoverColor: Theme.of(context).colorScheme.secondaryContainer,
|
||||
onPressed: () async {
|
||||
await showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return const FlowyDialog(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 20,
|
||||
),
|
||||
child: FileExporterWidget(),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+6
-6
@@ -12,8 +12,8 @@ import '../../../../startup/launch_configuration.dart';
|
||||
import '../../../../startup/startup.dart';
|
||||
import '../../../../startup/tasks/prelude.dart';
|
||||
|
||||
class SettingsFileLocationCustomzier extends StatefulWidget {
|
||||
const SettingsFileLocationCustomzier({
|
||||
class SettingsFileLocationCustomizer extends StatefulWidget {
|
||||
const SettingsFileLocationCustomizer({
|
||||
super.key,
|
||||
required this.cubit,
|
||||
});
|
||||
@@ -21,13 +21,13 @@ class SettingsFileLocationCustomzier extends StatefulWidget {
|
||||
final SettingsLocationCubit cubit;
|
||||
|
||||
@override
|
||||
State<SettingsFileLocationCustomzier> createState() =>
|
||||
SettingsFileLocationCustomzierState();
|
||||
State<SettingsFileLocationCustomizer> createState() =>
|
||||
SettingsFileLocationCustomizerState();
|
||||
}
|
||||
|
||||
@visibleForTesting
|
||||
class SettingsFileLocationCustomzierState
|
||||
extends State<SettingsFileLocationCustomzier> {
|
||||
class SettingsFileLocationCustomizerState
|
||||
extends State<SettingsFileLocationCustomizer> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider<SettingsLocationCubit>.value(
|
||||
|
||||
+126
-38
@@ -1,15 +1,22 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:appflowy/plugins/document/application/prelude.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/util/file_picker/file_picker_service.dart';
|
||||
import 'package:appflowy/workspace/application/settings/settings_file_exporter_cubit.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database/database_entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:dartz/dartz.dart' as dartz;
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart' hide WidgetBuilder;
|
||||
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pbserver.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/workspace.pb.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:tuple/tuple.dart';
|
||||
import '../../../../generated/locale_keys.g.dart';
|
||||
|
||||
class FileExporterWidget extends StatefulWidget {
|
||||
@@ -22,24 +29,44 @@ class FileExporterWidget extends StatefulWidget {
|
||||
class _FileExporterWidgetState extends State<FileExporterWidget> {
|
||||
// Map<String, List<String>> _selectedPages = {};
|
||||
|
||||
SettingsFileExporterCubit? cubit;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
FlowyText.medium(
|
||||
LocaleKeys.settings_files_selectFiles.tr(),
|
||||
fontSize: 16.0,
|
||||
),
|
||||
const VSpace(8),
|
||||
Expanded(child: _buildFileSelector(context)),
|
||||
const VSpace(8),
|
||||
_buildButtons(context)
|
||||
],
|
||||
return FutureBuilder<dartz.Either<WorkspaceSettingPB, FlowyError>>(
|
||||
future: FolderEventReadCurrentWorkspace().send(),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasData &&
|
||||
snapshot.connectionState == ConnectionState.done) {
|
||||
final workspaces = snapshot.data?.getLeftOrNull<WorkspaceSettingPB>();
|
||||
if (workspaces != null) {
|
||||
final apps = workspaces.workspace.apps.items;
|
||||
cubit ??= SettingsFileExporterCubit(apps: apps);
|
||||
return BlocProvider<SettingsFileExporterCubit>.value(
|
||||
value: cubit!,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
FlowyText.medium(
|
||||
LocaleKeys.settings_files_selectFiles.tr(),
|
||||
fontSize: 16.0,
|
||||
),
|
||||
const VSpace(8),
|
||||
const Expanded(child: _ExpandedList()),
|
||||
const VSpace(8),
|
||||
_buildButtons()
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
return const CircularProgressIndicator();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Row _buildButtons(BuildContext context) {
|
||||
Widget _buildButtons() {
|
||||
return Row(
|
||||
children: [
|
||||
const Spacer(),
|
||||
@@ -55,8 +82,28 @@ class _FileExporterWidgetState extends State<FileExporterWidget> {
|
||||
onPressed: () async {
|
||||
await getIt<FilePickerService>()
|
||||
.getDirectoryPath()
|
||||
.then((exportPath) {
|
||||
Navigator.of(context).pop();
|
||||
.then((exportPath) async {
|
||||
if (exportPath != null && cubit != null) {
|
||||
final views = cubit!.state.selectedViews;
|
||||
final result =
|
||||
await _AppFlowyFileExporter.exportToPath(exportPath, views);
|
||||
if (result.item1) {
|
||||
// success
|
||||
_showToast(LocaleKeys.settings_files_exportFileSuccess.tr());
|
||||
} else {
|
||||
_showToast(
|
||||
LocaleKeys.settings_files_exportFileFail.tr() +
|
||||
result.item2.join('\n'),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
_showToast(LocaleKeys.settings_files_exportFileFail.tr());
|
||||
}
|
||||
if (mounted) {
|
||||
Navigator.of(context).popUntil(
|
||||
(router) => router.settings.name == '/',
|
||||
);
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
@@ -64,24 +111,14 @@ class _FileExporterWidgetState extends State<FileExporterWidget> {
|
||||
);
|
||||
}
|
||||
|
||||
FutureBuilder<dartz.Either<WorkspaceSettingPB, FlowyError>>
|
||||
_buildFileSelector(BuildContext context) {
|
||||
return FutureBuilder<dartz.Either<WorkspaceSettingPB, FlowyError>>(
|
||||
future: FolderEventReadCurrentWorkspace().send(),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasData &&
|
||||
snapshot.connectionState == ConnectionState.done) {
|
||||
final workspaces = snapshot.data?.getLeftOrNull<WorkspaceSettingPB>();
|
||||
if (workspaces != null) {
|
||||
final apps = workspaces.workspace.apps.items;
|
||||
return BlocProvider<SettingsFileExporterCubit>(
|
||||
create: (_) => SettingsFileExporterCubit(apps: apps),
|
||||
child: const _ExpandedList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
return const CircularProgressIndicator();
|
||||
},
|
||||
void _showToast(String message) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: FlowyText(
|
||||
message,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -131,9 +168,9 @@ class _ExpandedListState extends State<_ExpandedList> {
|
||||
final apps = state.apps;
|
||||
final expanded = state.expanded;
|
||||
final selectedItems = state.selectedItems;
|
||||
final isExpaned = expanded[index] == true;
|
||||
final isExpanded = expanded[index] == true;
|
||||
List<Widget> expandedChildren = [];
|
||||
if (isExpaned) {
|
||||
if (isExpanded) {
|
||||
for (var i = 0; i < selectedItems[index].length; i++) {
|
||||
final name = apps[index].belongings.items[i].name;
|
||||
final checkbox = CheckboxListTile(
|
||||
@@ -160,7 +197,7 @@ class _ExpandedListState extends State<_ExpandedList> {
|
||||
child: ListTile(
|
||||
title: FlowyText.medium(apps[index].name),
|
||||
trailing: Icon(
|
||||
isExpaned
|
||||
isExpanded
|
||||
? Icons.arrow_drop_down_rounded
|
||||
: Icons.arrow_drop_up_rounded,
|
||||
),
|
||||
@@ -182,3 +219,54 @@ extension AppFlowy on dartz.Either {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
class _AppFlowyFileExporter {
|
||||
static Future<Tuple2<bool, List<String>>> exportToPath(
|
||||
String path,
|
||||
List<ViewPB> views,
|
||||
) async {
|
||||
final failedFileNames = <String>[];
|
||||
final Map<String, int> names = {};
|
||||
final documentService = DocumentService();
|
||||
for (final view in views) {
|
||||
String? content;
|
||||
String? fileExtension;
|
||||
switch (view.layout) {
|
||||
case ViewLayoutTypePB.Document:
|
||||
final document = await documentService.openDocument(view: view);
|
||||
document.fold(
|
||||
(l) => content = l.content,
|
||||
(r) => Log.error(r),
|
||||
);
|
||||
fileExtension = 'afdoc';
|
||||
break;
|
||||
default:
|
||||
final result = await exportDatabase(view.id);
|
||||
result.fold(
|
||||
(pb) => content = pb.data,
|
||||
(r) => Log.error(r),
|
||||
);
|
||||
fileExtension = 'afdb';
|
||||
break;
|
||||
}
|
||||
if (content != null) {
|
||||
final count = names.putIfAbsent(view.name, () => 0);
|
||||
final name = count == 0 ? view.name : '${view.name}($count)';
|
||||
final file = File(p.join(path, '$name.$fileExtension'));
|
||||
await file.writeAsString(content!);
|
||||
names[view.name] = count + 1;
|
||||
} else {
|
||||
failedFileNames.add(view.name);
|
||||
}
|
||||
}
|
||||
|
||||
return Tuple2(failedFileNames.isEmpty, failedFileNames);
|
||||
}
|
||||
}
|
||||
|
||||
Future<dartz.Either<ExportCSVPB, FlowyError>> exportDatabase(
|
||||
String viewId,
|
||||
) async {
|
||||
final payload = DatabaseViewIdPB.create()..value = viewId;
|
||||
return DatabaseEventExportCSV(payload).send();
|
||||
}
|
||||
+10
-11
@@ -1,3 +1,4 @@
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/settings_export_file_widget.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/settings_file_customize_location_view.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
@@ -14,22 +15,20 @@ class SettingsFileSystemView extends StatefulWidget {
|
||||
|
||||
class _SettingsFileSystemViewState extends State<SettingsFileSystemView> {
|
||||
final _locationCubit = SettingsLocationCubit()..fetchLocation();
|
||||
late final _items = [
|
||||
SettingsFileLocationCustomizer(
|
||||
cubit: _locationCubit,
|
||||
),
|
||||
const SettingsExportFileWidget()
|
||||
];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListView.separated(
|
||||
itemBuilder: (context, index) {
|
||||
if (index == 0) {
|
||||
return SettingsFileLocationCustomzier(
|
||||
cubit: _locationCubit,
|
||||
);
|
||||
} else if (index == 1) {
|
||||
// return _buildExportDatabaseButton();
|
||||
}
|
||||
return Container();
|
||||
},
|
||||
shrinkWrap: true,
|
||||
itemBuilder: (context, index) => _items[index],
|
||||
separatorBuilder: (context, index) => const Divider(),
|
||||
itemCount: 2, // make the divider taking effect.
|
||||
itemCount: _items.length,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -588,6 +588,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.appflowy.macos;
|
||||
PRODUCT_NAME = AppFlowy;
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
|
||||
@@ -38,7 +38,7 @@ String languageFromLocale(Locale locale) {
|
||||
case "hu":
|
||||
return "Magyar";
|
||||
case "id":
|
||||
return "Bahasa";
|
||||
return "Bahasa Indonesia";
|
||||
case "it":
|
||||
return "Italiano";
|
||||
case "ja":
|
||||
|
||||
@@ -132,6 +132,21 @@ class FlowyHoverContainer extends StatelessWidget {
|
||||
width: style.borderWidth,
|
||||
);
|
||||
|
||||
final theme = Theme.of(context);
|
||||
final textTheme = theme.textTheme;
|
||||
final iconTheme = theme.iconTheme;
|
||||
// override text's theme with foregroundColorOnHover when it is hovered
|
||||
final hoverTheme = theme.copyWith(
|
||||
textTheme: textTheme.copyWith(
|
||||
bodyMedium: textTheme.bodyMedium?.copyWith(
|
||||
color: style.foregroundColorOnHover ?? theme.colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
iconTheme: iconTheme.copyWith(
|
||||
color: style.foregroundColorOnHover ?? theme.colorScheme.onSurface,
|
||||
),
|
||||
);
|
||||
|
||||
return Container(
|
||||
margin: style.contentMargin,
|
||||
decoration: BoxDecoration(
|
||||
@@ -139,19 +154,8 @@ class FlowyHoverContainer extends StatelessWidget {
|
||||
color: style.hoverColor ?? Theme.of(context).colorScheme.secondary,
|
||||
borderRadius: style.borderRadius,
|
||||
),
|
||||
child:
|
||||
//override text's theme with foregroundColorOnHover when it is hovered
|
||||
Theme(
|
||||
data: Theme.of(context).copyWith(
|
||||
textTheme: Theme.of(context).textTheme.copyWith(
|
||||
bodyMedium: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: style.foregroundColorOnHover ??
|
||||
Theme.of(context).colorScheme.onSurface),
|
||||
),
|
||||
iconTheme: Theme.of(context).iconTheme.copyWith(
|
||||
color: style.foregroundColorOnHover ??
|
||||
Theme.of(context).colorScheme.onSurface),
|
||||
),
|
||||
child: Theme(
|
||||
data: hoverTheme,
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
|
||||
@@ -45,10 +45,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: appflowy_editor
|
||||
sha256: a1dbca3d7d33f4669f1d44bfa1e06b6646f46726c219921b8a59d9ee22bf252d
|
||||
sha256: "6f7d2b0b54ca1049cb396229549d228b5bbd7ea6d09f1f7325a20db2d7586a5f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.1.9"
|
||||
version: "0.1.12"
|
||||
appflowy_popover:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
||||
@@ -15,7 +15,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
|
||||
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
|
||||
# Read more about iOS versioning at
|
||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||
version: 0.1.2
|
||||
version: 0.1.6
|
||||
|
||||
environment:
|
||||
sdk: ">=2.19.0 <3.0.0"
|
||||
@@ -42,7 +42,7 @@ dependencies:
|
||||
git:
|
||||
url: https://github.com/AppFlowy-IO/appflowy-board.git
|
||||
ref: a183c57
|
||||
appflowy_editor: ^0.1.9
|
||||
appflowy_editor: ^0.1.12
|
||||
appflowy_popover:
|
||||
path: packages/appflowy_popover
|
||||
|
||||
|
||||
Generated
+1
@@ -1141,6 +1141,7 @@ dependencies = [
|
||||
"bytes",
|
||||
"chrono",
|
||||
"crossbeam-utils",
|
||||
"csv",
|
||||
"dashmap",
|
||||
"database-model",
|
||||
"diesel",
|
||||
|
||||
@@ -47,6 +47,7 @@ atomic_refcell = "0.1.9"
|
||||
crossbeam-utils = "0.8.15"
|
||||
async-stream = "0.3.4"
|
||||
parking_lot = "0.12.1"
|
||||
csv = "1.1.6"
|
||||
|
||||
[dev-dependencies]
|
||||
flowy-test = { path = "../flowy-test" }
|
||||
|
||||
@@ -203,3 +203,9 @@ pub struct DatabaseLayoutIdPB {
|
||||
#[pb(index = 2)]
|
||||
pub layout: LayoutTypePB,
|
||||
}
|
||||
|
||||
#[derive(Clone, ProtoBuf, Default, Debug)]
|
||||
pub struct ExportCSVPB {
|
||||
#[pb(index = 1)]
|
||||
pub data: String,
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
use crate::entities::*;
|
||||
use crate::manager::DatabaseManager;
|
||||
use crate::services::cell::{FromCellString, ToCellChangesetString, TypeCellData};
|
||||
use crate::services::export::CSVExport;
|
||||
use crate::services::field::{
|
||||
default_type_option_builder_from_type, select_type_option_from_field_rev,
|
||||
type_option_builder_from_json_str, DateCellChangeset, DateChangesetPB, SelectOptionCellChangeset,
|
||||
@@ -644,3 +645,16 @@ pub(crate) async fn get_calendar_event_handler(
|
||||
Some(event) => data_result_ok(event),
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "debug", skip(data, manager), err)]
|
||||
pub(crate) async fn export_csv_handler(
|
||||
data: AFPluginData<DatabaseViewIdPB>,
|
||||
manager: AFPluginState<Arc<DatabaseManager>>,
|
||||
) -> DataResult<ExportCSVPB, FlowyError> {
|
||||
let params = data.into_inner();
|
||||
let database_editor = manager.get_database_editor(¶ms.value).await?;
|
||||
let content = CSVExport
|
||||
.export_database(¶ms.value, &database_editor)
|
||||
.await?;
|
||||
data_result_ok(ExportCSVPB { data: content })
|
||||
}
|
||||
@@ -55,7 +55,8 @@ pub fn init(database_manager: Arc<DatabaseManager>) -> AFPlugin {
|
||||
.event(DatabaseEvent::GetCalendarEvent, get_calendar_event_handler)
|
||||
// Layout setting
|
||||
.event(DatabaseEvent::SetLayoutSetting, set_layout_setting_handler)
|
||||
.event(DatabaseEvent::GetLayoutSetting, get_layout_setting_handler);
|
||||
.event(DatabaseEvent::GetLayoutSetting, get_layout_setting_handler)
|
||||
.event(DatabaseEvent::ExportCSV, export_csv_handler);
|
||||
|
||||
plugin
|
||||
}
|
||||
@@ -256,4 +257,7 @@ pub enum DatabaseEvent {
|
||||
|
||||
#[event(input = "MoveCalendarEventPB")]
|
||||
MoveCalendarEvent = 119,
|
||||
|
||||
#[event(input = "DatabaseViewIdPB", output = "ExportCSVPB")]
|
||||
ExportCSV = 120,
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
use crate::entities::FieldType;
|
||||
|
||||
use crate::services::cell::TypeCellData;
|
||||
use crate::services::database::DatabaseEditor;
|
||||
use crate::services::field::{
|
||||
CheckboxTypeOptionPB, ChecklistTypeOptionPB, DateCellData, DateTypeOptionPB,
|
||||
MultiSelectTypeOptionPB, NumberTypeOptionPB, RichTextTypeOptionPB, SingleSelectTypeOptionPB,
|
||||
URLCellData,
|
||||
};
|
||||
use database_model::{FieldRevision, TypeOptionDataDeserializer};
|
||||
use flowy_error::{FlowyError, FlowyResult};
|
||||
use indexmap::IndexMap;
|
||||
use serde::Serialize;
|
||||
use serde_json::{json, Map, Value};
|
||||
use std::collections::HashMap;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct ExportField {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub field_type: i64,
|
||||
pub visibility: bool,
|
||||
pub width: i64,
|
||||
pub type_options: HashMap<String, Value>,
|
||||
pub is_primary: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
struct ExportCell {
|
||||
data: String,
|
||||
field_type: FieldType,
|
||||
}
|
||||
|
||||
impl From<&Arc<FieldRevision>> for ExportField {
|
||||
fn from(field_rev: &Arc<FieldRevision>) -> Self {
|
||||
let field_type = FieldType::from(field_rev.ty);
|
||||
let mut type_options: HashMap<String, Value> = HashMap::new();
|
||||
|
||||
field_rev
|
||||
.type_options
|
||||
.iter()
|
||||
.filter(|(k, _)| k == &&field_rev.ty.to_string())
|
||||
.for_each(|(k, s)| {
|
||||
let value = match field_type {
|
||||
FieldType::RichText => {
|
||||
let pb = RichTextTypeOptionPB::from_json_str(s);
|
||||
serde_json::to_value(pb).unwrap()
|
||||
},
|
||||
FieldType::Number => {
|
||||
let pb = NumberTypeOptionPB::from_json_str(s);
|
||||
let mut map = Map::new();
|
||||
map.insert("format".to_string(), json!(pb.format as u8));
|
||||
map.insert("scale".to_string(), json!(pb.scale));
|
||||
map.insert("symbol".to_string(), json!(pb.symbol));
|
||||
map.insert("name".to_string(), json!(pb.name));
|
||||
Value::Object(map)
|
||||
},
|
||||
FieldType::DateTime => {
|
||||
let pb = DateTypeOptionPB::from_json_str(s);
|
||||
let mut map = Map::new();
|
||||
map.insert("date_format".to_string(), json!(pb.date_format as u8));
|
||||
map.insert("time_format".to_string(), json!(pb.time_format as u8));
|
||||
map.insert("field_type".to_string(), json!(FieldType::DateTime as u8));
|
||||
Value::Object(map)
|
||||
},
|
||||
FieldType::SingleSelect => {
|
||||
let pb = SingleSelectTypeOptionPB::from_json_str(s);
|
||||
let value = serde_json::to_string(&pb).unwrap();
|
||||
let mut map = Map::new();
|
||||
map.insert("content".to_string(), Value::String(value));
|
||||
Value::Object(map)
|
||||
},
|
||||
FieldType::MultiSelect => {
|
||||
let pb = MultiSelectTypeOptionPB::from_json_str(s);
|
||||
let value = serde_json::to_string(&pb).unwrap();
|
||||
let mut map = Map::new();
|
||||
map.insert("content".to_string(), Value::String(value));
|
||||
Value::Object(map)
|
||||
},
|
||||
FieldType::Checkbox => {
|
||||
let pb = CheckboxTypeOptionPB::from_json_str(s);
|
||||
serde_json::to_value(pb).unwrap()
|
||||
},
|
||||
FieldType::URL => {
|
||||
let pb = RichTextTypeOptionPB::from_json_str(s);
|
||||
serde_json::to_value(pb).unwrap()
|
||||
},
|
||||
FieldType::Checklist => {
|
||||
let pb = ChecklistTypeOptionPB::from_json_str(s);
|
||||
let value = serde_json::to_string(&pb).unwrap();
|
||||
let mut map = Map::new();
|
||||
map.insert("content".to_string(), Value::String(value));
|
||||
Value::Object(map)
|
||||
},
|
||||
};
|
||||
type_options.insert(k.clone(), value);
|
||||
});
|
||||
Self {
|
||||
id: field_rev.id.clone(),
|
||||
name: field_rev.name.clone(),
|
||||
field_type: field_rev.ty as i64,
|
||||
visibility: true,
|
||||
width: 100,
|
||||
type_options,
|
||||
is_primary: field_rev.is_primary,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CSVExport;
|
||||
impl CSVExport {
|
||||
pub async fn export_database(
|
||||
&self,
|
||||
view_id: &str,
|
||||
database_editor: &Arc<DatabaseEditor>,
|
||||
) -> FlowyResult<String> {
|
||||
let mut wtr = csv::Writer::from_writer(vec![]);
|
||||
let row_revs = database_editor.get_all_row_revs(view_id).await?;
|
||||
let field_revs = database_editor.get_field_revs(None).await?;
|
||||
|
||||
// Write fields
|
||||
let field_records = field_revs
|
||||
.iter()
|
||||
.map(|field| ExportField::from(field))
|
||||
.map(|field| serde_json::to_string(&field).unwrap())
|
||||
.collect::<Vec<String>>();
|
||||
|
||||
wtr
|
||||
.write_record(&field_records)
|
||||
.map_err(|e| FlowyError::internal().context(e))?;
|
||||
|
||||
// Write rows
|
||||
let mut field_by_field_id = IndexMap::new();
|
||||
field_revs.into_iter().for_each(|field| {
|
||||
field_by_field_id.insert(field.id.clone(), field);
|
||||
});
|
||||
for row_rev in row_revs {
|
||||
let cells = field_by_field_id
|
||||
.iter()
|
||||
.map(|(field_id, field)| {
|
||||
let field_type = FieldType::from(field.ty);
|
||||
let data = row_rev
|
||||
.cells
|
||||
.get(field_id)
|
||||
.map(|cell| TypeCellData::try_from(cell))
|
||||
.map(|data| {
|
||||
data
|
||||
.map(|data| match field_type {
|
||||
FieldType::DateTime => {
|
||||
match serde_json::from_str::<DateCellData>(&data.cell_str) {
|
||||
Ok(cell_data) => cell_data.timestamp.unwrap_or_default().to_string(),
|
||||
Err(_) => "".to_string(),
|
||||
}
|
||||
},
|
||||
FieldType::URL => match serde_json::from_str::<URLCellData>(&data.cell_str) {
|
||||
Ok(cell_data) => cell_data.content,
|
||||
Err(_) => "".to_string(),
|
||||
},
|
||||
_ => data.cell_str,
|
||||
})
|
||||
.unwrap_or_default()
|
||||
})
|
||||
.unwrap_or_else(|| "".to_string());
|
||||
let cell = ExportCell { data, field_type };
|
||||
serde_json::to_string(&cell).unwrap()
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if let Err(e) = wtr.write_record(&cells) {
|
||||
tracing::warn!("CSV failed to write record: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
let data = wtr
|
||||
.into_inner()
|
||||
.map_err(|e| FlowyError::internal().context(e))?;
|
||||
let csv = String::from_utf8(data).map_err(|e| FlowyError::internal().context(e))?;
|
||||
Ok(csv)
|
||||
}
|
||||
}
|
||||
+2
@@ -168,6 +168,7 @@ impl ToString for DateCellData {
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Copy, EnumIter, Serialize, Deserialize, ProtoBuf_Enum)]
|
||||
#[repr(u8)]
|
||||
pub enum DateFormat {
|
||||
Local = 0,
|
||||
US = 1,
|
||||
@@ -216,6 +217,7 @@ impl DateFormat {
|
||||
#[derive(
|
||||
Clone, Copy, PartialEq, Eq, EnumIter, Debug, Hash, Serialize, Deserialize, ProtoBuf_Enum,
|
||||
)]
|
||||
#[repr(u8)]
|
||||
pub enum TimeFormat {
|
||||
TwelveHour = 0,
|
||||
TwentyFourHour = 1,
|
||||
|
||||
+1
@@ -14,6 +14,7 @@ lazy_static! {
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, EnumIter, Serialize, Deserialize, ProtoBuf_Enum)]
|
||||
#[repr(u8)]
|
||||
pub enum NumberFormat {
|
||||
Num = 0,
|
||||
USD = 1,
|
||||
|
||||
+11
-1
@@ -216,7 +216,17 @@ impl TypeOptionCellDataCompare for NumberTypeOptionPB {
|
||||
cell_data: &<Self as TypeOption>::CellData,
|
||||
other_cell_data: &<Self as TypeOption>::CellData,
|
||||
) -> Ordering {
|
||||
cell_data.0.cmp(&other_cell_data.0)
|
||||
let left = NumberCellData::from_format_str(&cell_data.0, self.sign_positive, &self.format);
|
||||
let right =
|
||||
NumberCellData::from_format_str(&other_cell_data.0, self.sign_positive, &self.format);
|
||||
match (left, right) {
|
||||
(Ok(left), Ok(right)) => {
|
||||
return left.decimal().cmp(&right.decimal());
|
||||
},
|
||||
(Ok(_), Err(_)) => Ordering::Greater,
|
||||
(Err(_), Ok(_)) => Ordering::Less,
|
||||
(Err(_), Err(_)) => Ordering::Equal,
|
||||
}
|
||||
}
|
||||
}
|
||||
impl std::default::Default for NumberTypeOptionPB {
|
||||
|
||||
+1
-1
@@ -37,7 +37,7 @@ impl TypeOptionBuilder for RichTextTypeOptionBuilder {
|
||||
pub struct RichTextTypeOptionPB {
|
||||
#[pb(index = 1)]
|
||||
#[serde(default)]
|
||||
data: String,
|
||||
pub data: String,
|
||||
}
|
||||
impl_type_option!(RichTextTypeOptionPB, FieldType::RichText);
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ mod util;
|
||||
pub mod cell;
|
||||
pub mod database;
|
||||
pub mod database_view;
|
||||
pub mod export;
|
||||
pub mod field;
|
||||
pub mod filter;
|
||||
pub mod group;
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
use crate::database::database_editor::DatabaseEditorTest;
|
||||
use flowy_database::services::export::CSVExport;
|
||||
|
||||
#[tokio::test]
|
||||
async fn export_test() {
|
||||
let test = DatabaseEditorTest::new_grid().await;
|
||||
|
||||
let s = CSVExport
|
||||
.export_database(&test.view_id, &test.editor)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let mut reader = csv::Reader::from_reader(s.as_bytes());
|
||||
for header in reader.headers() {
|
||||
println!("{:?}", header);
|
||||
}
|
||||
|
||||
let export_csv_records = reader.records();
|
||||
for record in export_csv_records {
|
||||
let record = record.unwrap();
|
||||
println!("{:?}", record);
|
||||
}
|
||||
}
|
||||
@@ -161,7 +161,7 @@ pub fn make_test_grid() -> BuildDatabaseContext {
|
||||
for field_type in FieldType::iter() {
|
||||
match field_type {
|
||||
FieldType::RichText => row_builder.insert_text_cell("DA"),
|
||||
FieldType::Number => row_builder.insert_number_cell("4"),
|
||||
FieldType::Number => row_builder.insert_number_cell("14"),
|
||||
FieldType::DateTime => row_builder.insert_date_cell("1668704685"),
|
||||
FieldType::SingleSelect => {
|
||||
row_builder.insert_single_select_cell(|mut options| options.remove(0))
|
||||
|
||||
@@ -9,4 +9,5 @@ mod layout_test;
|
||||
mod snapshot_test;
|
||||
mod sort_test;
|
||||
|
||||
mod export_test;
|
||||
mod mock_data;
|
||||
@@ -212,7 +212,7 @@ async fn sort_number_by_descending_test() {
|
||||
let scripts = vec![
|
||||
AssertCellContentOrder {
|
||||
field_id: number_field.id.clone(),
|
||||
orders: vec!["$1", "$2", "$3", "$4", "", "$5"],
|
||||
orders: vec!["$1", "$2", "$3", "$14", "", "$5"],
|
||||
},
|
||||
InsertSort {
|
||||
field_rev: number_field.clone(),
|
||||
@@ -220,7 +220,7 @@ async fn sort_number_by_descending_test() {
|
||||
},
|
||||
AssertCellContentOrder {
|
||||
field_id: number_field.id.clone(),
|
||||
orders: vec!["$5", "$4", "$3", "$2", "$1", ""],
|
||||
orders: vec!["$14", "$5", "$3", "$2", "$1", ""],
|
||||
},
|
||||
];
|
||||
test.run_scripts(scripts).await;
|
||||
|
||||
@@ -2,175 +2,78 @@
|
||||
"document": {
|
||||
"type": "editor",
|
||||
"children": [
|
||||
{ "type": "cover" },
|
||||
{
|
||||
"type": "text",
|
||||
"attributes": {
|
||||
"subtype": "heading",
|
||||
"heading": "h1"
|
||||
},
|
||||
"attributes": { "subtype": "heading", "heading": "h1" },
|
||||
"delta": [{ "insert": "Welcome to AppFlowy!" }]
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"attributes": { "subtype": "heading", "heading": "h2" },
|
||||
"delta": [{ "insert": "Here are the basics" }]
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"attributes": { "subtype": "checkbox", "checkbox": null },
|
||||
"delta": [{ "insert": "Click anywhere and just start typing." }]
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"attributes": { "subtype": "checkbox", "checkbox": false },
|
||||
"delta": [
|
||||
{
|
||||
"insert": "🌟 Welcome to AppFlowy!"
|
||||
}
|
||||
"insert": "Highlight ",
|
||||
"attributes": { "backgroundColor": "0x4dffeb3b" }
|
||||
},
|
||||
{ "insert": "any text, and use the editing menu to " },
|
||||
{ "insert": "style", "attributes": { "italic": true } },
|
||||
{ "insert": " " },
|
||||
{ "insert": "your", "attributes": { "bold": true } },
|
||||
{ "insert": " " },
|
||||
{ "insert": "writing", "attributes": { "underline": true } },
|
||||
{ "insert": " " },
|
||||
{ "insert": "however", "attributes": { "code": true } },
|
||||
{ "insert": " you " },
|
||||
{ "insert": "like.", "attributes": { "strikethrough": true } }
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"attributes": {
|
||||
"subtype": "heading",
|
||||
"heading": "h2"
|
||||
},
|
||||
"attributes": { "subtype": "checkbox", "checkbox": null },
|
||||
"delta": [
|
||||
{
|
||||
"insert": "Here are the basics"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"attributes": {
|
||||
"subtype": "checkbox",
|
||||
"checkbox": null
|
||||
},
|
||||
"delta": [
|
||||
{
|
||||
"insert": "Click anywhere and just start typing."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"attributes": {
|
||||
"subtype": "checkbox",
|
||||
"checkbox": null
|
||||
},
|
||||
"delta": [
|
||||
{
|
||||
"insert": "Highlight",
|
||||
"attributes": {
|
||||
"backgroundColor": "0x6000BCF0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"insert": " any text, and use the editing menu to "
|
||||
},
|
||||
{
|
||||
"insert": "style",
|
||||
"attributes": {
|
||||
"italic": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"insert": " "
|
||||
},
|
||||
{
|
||||
"insert": "your",
|
||||
"attributes": {
|
||||
"bold": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"insert": " "
|
||||
},
|
||||
{
|
||||
"insert": "writing",
|
||||
"attributes": {
|
||||
"underline": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"insert": " "
|
||||
},
|
||||
{
|
||||
"insert": "however",
|
||||
"attributes": {
|
||||
"code": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"insert": " you "
|
||||
},
|
||||
{
|
||||
"insert": "like.",
|
||||
"attributes": {
|
||||
"strikethrough": true
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"attributes": {
|
||||
"subtype": "checkbox",
|
||||
"checkbox": null
|
||||
},
|
||||
"delta": [
|
||||
{
|
||||
"insert": "As soon as you type "
|
||||
},
|
||||
{ "insert": "As soon as you type " },
|
||||
{
|
||||
"insert": "/",
|
||||
"attributes": {
|
||||
"code": true
|
||||
}
|
||||
"attributes": { "code": true, "color": "0xff00b5ff" }
|
||||
},
|
||||
{ "insert": " a menu will pop up. Select " },
|
||||
{
|
||||
"insert": " a menu will pop up. Select different types of content blocks you can add."
|
||||
}
|
||||
"insert": "different types",
|
||||
"attributes": { "backgroundColor": "0x4d9c27b0" }
|
||||
},
|
||||
{ "insert": " of content blocks you can add." }
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"attributes": {
|
||||
"subtype": "checkbox",
|
||||
"checkbox": null
|
||||
},
|
||||
"attributes": { "subtype": "checkbox", "checkbox": null },
|
||||
"delta": [
|
||||
{
|
||||
"insert": "Type "
|
||||
},
|
||||
{
|
||||
"insert": "/",
|
||||
"attributes": {
|
||||
"code": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"insert": " followed by "
|
||||
},
|
||||
{
|
||||
"insert": "/bullet",
|
||||
"attributes": {
|
||||
"code": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"insert": " or "
|
||||
},
|
||||
{
|
||||
"insert": "/c.",
|
||||
"attributes": {
|
||||
"code": true
|
||||
}
|
||||
}
|
||||
{ "insert": "Type " },
|
||||
{ "insert": "/", "attributes": { "code": true } },
|
||||
{ "insert": " followed by " },
|
||||
{ "insert": "/bullet", "attributes": { "code": true } },
|
||||
{ "insert": " or " },
|
||||
{ "insert": "/num", "attributes": { "code": true } },
|
||||
{ "insert": " to create a list.", "attributes": { "code": false } }
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"attributes": {
|
||||
"subtype": "checkbox",
|
||||
"checkbox": true
|
||||
},
|
||||
"attributes": { "subtype": "checkbox", "checkbox": true },
|
||||
"delta": [
|
||||
{
|
||||
"insert": "Click "
|
||||
},
|
||||
{
|
||||
"insert": "+ New Page ",
|
||||
"attributes": {
|
||||
"code": true
|
||||
}
|
||||
},
|
||||
{ "insert": "Click " },
|
||||
{ "insert": "+ New Page ", "attributes": { "code": true } },
|
||||
{
|
||||
"insert": "button at the bottom of your sidebar to add a new page."
|
||||
}
|
||||
@@ -178,32 +81,24 @@
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"attributes": {
|
||||
"subtype": "checkbox",
|
||||
"checkbox": null
|
||||
},
|
||||
"attributes": { "subtype": "checkbox", "checkbox": null },
|
||||
"delta": [
|
||||
{
|
||||
"insert": "Click "
|
||||
},
|
||||
{
|
||||
"insert": "+",
|
||||
"attributes": {
|
||||
"code": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"insert": " next to any page title in the sidebar to quickly add a new subpage."
|
||||
}
|
||||
{ "insert": "Click " },
|
||||
{ "insert": "+", "attributes": { "code": true } },
|
||||
{ "insert": " next to any page title in the sidebar to " },
|
||||
{ "insert": "quickly", "attributes": { "color": "0xff8427e0" } },
|
||||
{ "insert": " add a new subpage, " },
|
||||
{ "insert": "Document", "attributes": { "code": true } },
|
||||
{ "insert": ", ", "attributes": { "code": false } },
|
||||
{ "insert": "Grid", "attributes": { "code": true } },
|
||||
{ "insert": ", or ", "attributes": { "code": false } },
|
||||
{ "insert": "Kanban Board", "attributes": { "code": true } },
|
||||
{ "insert": ".", "attributes": { "code": false } }
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"attributes": {
|
||||
"checkbox": null
|
||||
},
|
||||
"delta": []
|
||||
},
|
||||
{ "type": "text", "delta": [] },
|
||||
{ "type": "divider" },
|
||||
{ "type": "text", "attributes": { "checkbox": null }, "delta": [] },
|
||||
{
|
||||
"type": "text",
|
||||
"attributes": {
|
||||
@@ -211,11 +106,7 @@
|
||||
"checkbox": null,
|
||||
"heading": "h2"
|
||||
},
|
||||
"delta": [
|
||||
{
|
||||
"insert": "Markdown"
|
||||
}
|
||||
]
|
||||
"delta": [{ "insert": "Keyboard shortcuts, markdown, and code block" }]
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
@@ -225,98 +116,64 @@
|
||||
"heading": null
|
||||
},
|
||||
"delta": [
|
||||
{ "insert": "Keyboard shortcuts " },
|
||||
{
|
||||
"insert": "Heading "
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"attributes": {
|
||||
"subtype": "number-list",
|
||||
"number": 2
|
||||
},
|
||||
"delta": [
|
||||
{
|
||||
"insert": "bold text",
|
||||
"insert": "guide",
|
||||
"attributes": {
|
||||
"bold": true,
|
||||
"defaultFormatting": true
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"attributes": {
|
||||
"subtype": "number-list",
|
||||
"number": 3
|
||||
},
|
||||
"delta": [
|
||||
{
|
||||
"insert": "italicized text",
|
||||
"attributes": {
|
||||
"italic": true
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"attributes": {
|
||||
"subtype": "number-list",
|
||||
"number": 4,
|
||||
"number-list": null
|
||||
},
|
||||
"delta": [
|
||||
{
|
||||
"insert": "Ordered List"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"attributes": {
|
||||
"number": 5,
|
||||
"subtype": "number-list"
|
||||
},
|
||||
"delta": [
|
||||
{
|
||||
"insert": "code",
|
||||
"attributes": {
|
||||
"code": true
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"attributes": {
|
||||
"number": 6,
|
||||
"subtype": "number-list"
|
||||
},
|
||||
"delta": [
|
||||
{
|
||||
"insert": "Strikethrough",
|
||||
"attributes": {
|
||||
"strikethrough": true
|
||||
"href": "https://appflowy.gitbook.io/docs/essential-documentation/shortcuts"
|
||||
}
|
||||
},
|
||||
{ "retain": 1, "attributes": { "strikethrough": true } }
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"attributes": {
|
||||
"subtype": "number-list",
|
||||
"number": 2,
|
||||
"heading": null
|
||||
},
|
||||
"delta": [
|
||||
{ "insert": "Markdown " },
|
||||
{
|
||||
"retain": 1,
|
||||
"insert": "reference",
|
||||
"attributes": {
|
||||
"strikethrough": true
|
||||
"href": "https://appflowy.gitbook.io/docs/essential-documentation/markdown"
|
||||
}
|
||||
},
|
||||
{ "retain": 1, "attributes": { "strikethrough": true } }
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"attributes": { "number": 3, "subtype": "number-list" },
|
||||
"delta": [
|
||||
{ "insert": "Type " },
|
||||
{ "insert": "/code", "attributes": { "code": true } },
|
||||
{
|
||||
"insert": " to insert a code block",
|
||||
"attributes": { "code": false }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"attributes": {
|
||||
"checkbox": null
|
||||
"subtype": "code_block",
|
||||
"number": 3,
|
||||
"heading": null,
|
||||
"number-list": null,
|
||||
"theme": "vs",
|
||||
"language": "rust"
|
||||
},
|
||||
"delta": []
|
||||
"delta": [
|
||||
{
|
||||
"insert": "// This is the main function.\nfn main() {\n // Print text to the console.\n println!(\"Hello World!\");\n}"
|
||||
},
|
||||
{ "retain": 1, "attributes": { "strikethrough": true } }
|
||||
]
|
||||
},
|
||||
{ "type": "text", "attributes": { "checkbox": null }, "delta": [] },
|
||||
{
|
||||
"type": "text",
|
||||
"attributes": {
|
||||
@@ -324,94 +181,72 @@
|
||||
"checkbox": null,
|
||||
"heading": "h2"
|
||||
},
|
||||
"delta": [
|
||||
{
|
||||
"insert": "Have a question?"
|
||||
}
|
||||
]
|
||||
"delta": [{ "insert": "Have a question❓" }]
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"attributes": {
|
||||
"subtype": "quote"
|
||||
},
|
||||
"attributes": { "subtype": "quote" },
|
||||
"delta": [
|
||||
{
|
||||
"insert": "Click "
|
||||
},
|
||||
{
|
||||
"insert": "?",
|
||||
"attributes": {
|
||||
"code": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"insert": " at the bottom right for help and support."
|
||||
}
|
||||
{ "insert": "Click " },
|
||||
{ "insert": "?", "attributes": { "code": true } },
|
||||
{ "insert": " at the bottom right for help and support." }
|
||||
]
|
||||
},
|
||||
{ "type": "text", "delta": [] },
|
||||
{
|
||||
"type": "callout",
|
||||
"children": [
|
||||
{ "type": "text", "delta": [] },
|
||||
{
|
||||
"type": "text",
|
||||
"attributes": { "subtype": "heading", "heading": "h2" },
|
||||
"delta": [{ "insert": "Like AppFlowy? Follow us:" }]
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"attributes": { "subtype": "bulleted-list" },
|
||||
"delta": [
|
||||
{
|
||||
"insert": "GitHub",
|
||||
"attributes": {
|
||||
"href": "https://github.com/AppFlowy-IO/AppFlowy"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"attributes": { "subtype": "bulleted-list" },
|
||||
"delta": [
|
||||
{
|
||||
"insert": "Twitter",
|
||||
"attributes": { "href": "https://twitter.com/appflowy" }
|
||||
},
|
||||
{ "insert": ": @appflowy" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"attributes": { "subtype": "bulleted-list" },
|
||||
"delta": [
|
||||
{
|
||||
"insert": "Newsletter",
|
||||
"attributes": { "href": "https://blog-appflowy.ghost.io/" }
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"attributes": { "emoji": "🥰" }
|
||||
},
|
||||
{ "type": "text", "delta": [] },
|
||||
{
|
||||
"type": "text",
|
||||
"attributes": { "subtype": null, "heading": null },
|
||||
"delta": []
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"attributes": {
|
||||
"subtype": "heading",
|
||||
"heading": "h2"
|
||||
},
|
||||
"delta": [
|
||||
{
|
||||
"insert": "Like AppFlowy? Follow us:"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"attributes": {
|
||||
"subtype": "bulleted-list",
|
||||
"quote": null
|
||||
},
|
||||
"delta": [
|
||||
{
|
||||
"insert": "GitHub",
|
||||
"attributes": {
|
||||
"href": "https://github.com/AppFlowy-IO/AppFlowy"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"attributes": {
|
||||
"subtype": "bulleted-list"
|
||||
},
|
||||
"delta": [
|
||||
{
|
||||
"insert": "Twitter: @appflowy"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"attributes": {
|
||||
"subtype": "bulleted-list"
|
||||
},
|
||||
"delta": [
|
||||
{
|
||||
"insert": "Newsletter",
|
||||
"attributes": {
|
||||
"href": "https://blog-appflowy.ghost.io/"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"attributes": {
|
||||
"subtype": null,
|
||||
"heading": null
|
||||
},
|
||||
"attributes": { "subtype": null, "heading": null },
|
||||
"delta": []
|
||||
}
|
||||
]
|
||||
|
||||
@@ -23,6 +23,11 @@ args[1]: The appflowy version to be built (github ref_name).
|
||||
assert(await repositoryRoot.exists(),
|
||||
'$repositoryRoot is an invalid directory. Please try again with a valid directory.\n\n$help');
|
||||
final appVersion = args[1];
|
||||
await _BuildTool(repositoryRoot: repositoryRoot.path, appVersion: appVersion)
|
||||
.run();
|
||||
String? arch;
|
||||
if (args.length > 2) arch = args[2];
|
||||
await _BuildTool(
|
||||
repositoryRoot: repositoryRoot.path,
|
||||
appVersion: appVersion,
|
||||
arch: arch,
|
||||
).run();
|
||||
}
|
||||
@@ -14,10 +14,12 @@ class _BuildTool {
|
||||
const _BuildTool({
|
||||
required this.repositoryRoot,
|
||||
required this.appVersion,
|
||||
this.arch,
|
||||
});
|
||||
|
||||
final String repositoryRoot;
|
||||
final String appVersion;
|
||||
final String? arch;
|
||||
|
||||
String get projectRoot =>
|
||||
[repositoryRoot, 'appflowy_flutter'].join(Platform.pathSeparator);
|
||||
@@ -30,8 +32,9 @@ class _BuildTool {
|
||||
|
||||
Future<String> get _commandForOS async {
|
||||
// Check the operating system and CPU architecture
|
||||
var os = Platform.operatingSystem;
|
||||
var arch = Platform.isMacOS ? await _architecture : Platform.localHostname;
|
||||
final os = Platform.operatingSystem;
|
||||
final arch = this.arch ??
|
||||
(Platform.isMacOS ? await _architecture : Platform.localHostname);
|
||||
|
||||
// Determine the appropriate command based on the OS and architecture
|
||||
if (os == 'windows') {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
[Setup]
|
||||
AppName=AppFlowy
|
||||
AppVersion={#AppVersion}
|
||||
AppPublisher=AppFlowy-IO
|
||||
WizardStyle=modern
|
||||
Compression=lzma2
|
||||
SolidCompression=yes
|
||||
@@ -9,8 +10,8 @@ DefaultGroupName=AppFlowy
|
||||
SetupIconFile=flowy_logo.ico
|
||||
UninstallDisplayIcon={app}\AppFlowy.exe
|
||||
UninstallDisplayName=AppFlowy
|
||||
AppPublisher=AppFlowy-IO
|
||||
VersionInfoVersion={#AppVersion}
|
||||
UsePreviousAppDir=no
|
||||
|
||||
[Files]
|
||||
Source: "AppFlowy\AppFlowy.exe";DestDir: "{app}";DestName: "AppFlowy.exe"
|
||||
@@ -18,4 +19,5 @@ Source: "AppFlowy\*";DestDir: "{app}"
|
||||
Source: "AppFlowy\data\*";DestDir: "{app}\data\"; Flags: recursesubdirs
|
||||
|
||||
[Icons]
|
||||
Name: "{group}\AppFlowy";Filename: "{app}\AppFlowy.exe"
|
||||
Name: "{userdesktop}\AppFlowy"; Filename: "{app}\AppFlowy.exe"
|
||||
Name: "{group}\AppFlowy"; Filename: "{app}\AppFlowy.exe"
|
||||
+14
-26
@@ -1,29 +1,17 @@
|
||||
|
||||
// @ts-check
|
||||
|
||||
/**
|
||||
* @type { import("@inlang/core/config").DefineConfig }
|
||||
*/
|
||||
export async function defineConfig(env) {
|
||||
const plugin = await env.$import(
|
||||
"https://cdn.jsdelivr.net/gh/samuelstroschein/inlang-plugin-json@1.1.1/dist/index.js"
|
||||
);
|
||||
const { default: pluginJson } = await env.$import(
|
||||
'https://cdn.jsdelivr.net/gh/samuelstroschein/inlang-plugin-json@2/dist/index.js'
|
||||
);
|
||||
|
||||
const { default: standardLintRules } = await env.$import(
|
||||
"https://cdn.jsdelivr.net/gh/inlang/standard-lint-rules@1.1.1/dist/index.js"
|
||||
);
|
||||
const { default: standardLintRules } = await env.$import(
|
||||
'https://cdn.jsdelivr.net/gh/inlang/standard-lint-rules@2/dist/index.js'
|
||||
);
|
||||
|
||||
const pluginConfig = {
|
||||
pathPattern: "./frontend/appflowy_flutter/assets/translations/{language}.json",
|
||||
};
|
||||
|
||||
return {
|
||||
referenceLanguage: "en",
|
||||
languages: await plugin.getLanguages({ ...env, pluginConfig }),
|
||||
readResources: (args) => plugin.readResources({ ...args, ...env, pluginConfig }),
|
||||
writeResources: (args) => plugin.writeResources({ ...args, ...env, pluginConfig }),
|
||||
lint: {
|
||||
rules: [standardLintRules()],
|
||||
},
|
||||
};
|
||||
}
|
||||
return {
|
||||
referenceLanguage: 'en',
|
||||
plugins: [pluginJson({
|
||||
pathPattern: './frontend/appflowy_flutter/assets/translations/{language}.json',
|
||||
variableReferencePattern: ["@:"]
|
||||
}), standardLintRules()]
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user