diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index 40f51c8132..ab1becf24f 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -34,6 +34,7 @@ jobs:
with:
channel: 'stable'
cache: true
+ flutter-version: '3.0.0'
- name: Cache Cargo
uses: actions/cache@v2
diff --git a/.github/workflows/dart_lint.yml b/.github/workflows/dart_lint.yml
index 81460cec1c..ca0536906e 100644
--- a/.github/workflows/dart_lint.yml
+++ b/.github/workflows/dart_lint.yml
@@ -23,7 +23,7 @@ jobs:
uses: actions/checkout@v2
- uses: subosito/flutter-action@v1
with:
- flutter-version: '2.10.0'
+ flutter-version: '3.0.0'
channel: "stable"
- name: Deps Flutter
run: flutter packages pub get
diff --git a/.github/workflows/dart_test.yml b/.github/workflows/dart_test.yml
index db479d2905..9938f02091 100644
--- a/.github/workflows/dart_test.yml
+++ b/.github/workflows/dart_test.yml
@@ -25,6 +25,7 @@ jobs:
- uses: subosito/flutter-action@v2
with:
channel: 'stable'
+ flutter-version: '3.0.0'
cache: true
- name: Cache Cargo
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 93dedaa43d..4d500c4659 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -50,6 +50,7 @@ jobs:
uses: subosito/flutter-action@v2
with:
channel: 'stable'
+ flutter-version: '3.0.0'
- name: Pre build
working-directory: frontend
@@ -98,6 +99,7 @@ jobs:
uses: subosito/flutter-action@v2
with:
channel: 'stable'
+ flutter-version: '3.0.0'
- name: Pre build
working-directory: frontend
diff --git a/frontend/Makefile.toml b/frontend/Makefile.toml
index 7c82e17ba7..c46687a2df 100644
--- a/frontend/Makefile.toml
+++ b/frontend/Makefile.toml
@@ -7,6 +7,7 @@ extend = [
{ path = "scripts/makefile/docker.toml" },
{ path = "scripts/makefile/env.toml" },
{ path = "scripts/makefile/flutter.toml" },
+ { path = "scripts/makefile/tool.toml" },
]
[config]
diff --git a/frontend/app_flowy/.vscode/launch.json b/frontend/app_flowy/.vscode/launch.json
index b271a1e6a1..e5ea3cdf6f 100644
--- a/frontend/app_flowy/.vscode/launch.json
+++ b/frontend/app_flowy/.vscode/launch.json
@@ -5,18 +5,30 @@
"version": "0.2.0",
"configurations": [
{
- "name": "app_flowy",
+ // This task builds the Rust and Dart code of AppFlowy.
+ "name": "Build",
"request": "launch",
"program": "${workspaceRoot}/lib/main.dart",
- "type": "dart",
"preLaunchTask": "build_flowy_sdk",
+ "type": "dart",
"env": {
"RUST_LOG": "debug"
},
"cwd": "${workspaceRoot}"
},
{
- "name": "app_flowy(trace)",
+ // This task only build the Dart code of AppFlowy.
+ "name": "Build (Dart)",
+ "request": "launch",
+ "program": "${workspaceRoot}/lib/main.dart",
+ "type": "dart",
+ "env": {
+ "RUST_LOG": "debug"
+ },
+ "cwd": "${workspaceRoot}"
+ },
+ {
+ "name": "Build (trace log)",
"request": "launch",
"program": "${workspaceRoot}/lib/main.dart",
"type": "dart",
@@ -27,7 +39,7 @@
"cwd": "${workspaceRoot}"
},
{
- "name": "app_flowy (profile mode)",
+ "name": "Build (profile mode)",
"request": "launch",
"type": "dart",
"flutterMode": "profile"
diff --git a/frontend/app_flowy/assets/images/grid/field/url.svg b/frontend/app_flowy/assets/images/grid/field/url.svg
new file mode 100644
index 0000000000..f00f5c7aa2
--- /dev/null
+++ b/frontend/app_flowy/assets/images/grid/field/url.svg
@@ -0,0 +1,3 @@
+
diff --git a/frontend/app_flowy/assets/translations/en.json b/frontend/app_flowy/assets/translations/en.json
index 8755f05952..4e6c8f3420 100644
--- a/frontend/app_flowy/assets/translations/en.json
+++ b/frontend/app_flowy/assets/translations/en.json
@@ -160,6 +160,7 @@
"numberFieldName": "Numbers",
"singleSelectFieldName": "Select",
"multiSelectFieldName": "Multiselect",
+ "urlFieldName": "URL",
"numberFormat": " Number format",
"dateFormat": " Date format",
"includeTime": " Include time",
@@ -168,6 +169,7 @@
"dateFormatLocal": "Month/Month/Day",
"dateFormatUS": "Month/Month/Day",
"timeFormat": " Time format",
+ "invalidTimeFormat": "Invalid format",
"timeFormatTwelveHour": "12 hour",
"timeFormatTwentyFourHour": "24 hour",
"addSelectOption": "Add an option",
diff --git a/frontend/app_flowy/lib/core/frameless_window.dart b/frontend/app_flowy/lib/core/frameless_window.dart
new file mode 100644
index 0000000000..a7d6417cd3
--- /dev/null
+++ b/frontend/app_flowy/lib/core/frameless_window.dart
@@ -0,0 +1,67 @@
+import 'package:flutter/services.dart';
+import 'package:flutter/material.dart';
+import 'dart:io' show Platform;
+
+class CocoaWindowChannel {
+ CocoaWindowChannel._();
+
+ final MethodChannel _channel = const MethodChannel("flutter/cocoaWindow");
+
+ static final CocoaWindowChannel instance = CocoaWindowChannel._();
+
+ Future setWindowPosition(Offset offset) async {
+ await _channel.invokeMethod("setWindowPosition", [offset.dx, offset.dy]);
+ }
+
+ Future> getWindowPosition() async {
+ final raw = await _channel.invokeMethod("getWindowPosition");
+ final arr = raw as List;
+ final List result = arr.map((s) => s as double).toList();
+ return result;
+ }
+
+ Future zoom() async {
+ await _channel.invokeMethod("zoom");
+ }
+}
+
+class MoveWindowDetector extends StatefulWidget {
+ const MoveWindowDetector({Key? key, this.child}) : super(key: key);
+
+ final Widget? child;
+
+ @override
+ _MoveWindowDetectorState createState() => _MoveWindowDetectorState();
+}
+
+class _MoveWindowDetectorState extends State {
+ double winX = 0;
+ double winY = 0;
+
+ @override
+ Widget build(BuildContext context) {
+ if (!Platform.isMacOS) {
+ return widget.child ?? Container();
+ }
+ return GestureDetector(
+ // https://stackoverflow.com/questions/52965799/flutter-gesturedetector-not-working-with-containers-in-stack
+ behavior: HitTestBehavior.translucent,
+ onDoubleTap: () async {
+ await CocoaWindowChannel.instance.zoom();
+ },
+ onPanStart: (DragStartDetails details) {
+ winX = details.globalPosition.dx;
+ winY = details.globalPosition.dy;
+ },
+ onPanUpdate: (DragUpdateDetails details) async {
+ final windowPos = await CocoaWindowChannel.instance.getWindowPosition();
+ final double dx = windowPos[0];
+ final double dy = windowPos[1];
+ final deltaX = details.globalPosition.dx - winX;
+ final deltaY = details.globalPosition.dy - winY;
+ await CocoaWindowChannel.instance.setWindowPosition(Offset(dx + deltaX, dy - deltaY));
+ },
+ child: widget.child,
+ );
+ }
+}
diff --git a/frontend/app_flowy/lib/user/presentation/skip_log_in_screen.dart b/frontend/app_flowy/lib/user/presentation/skip_log_in_screen.dart
index bad8000643..4a78815beb 100644
--- a/frontend/app_flowy/lib/user/presentation/skip_log_in_screen.dart
+++ b/frontend/app_flowy/lib/user/presentation/skip_log_in_screen.dart
@@ -88,8 +88,9 @@ class _SkipLogInScreenState extends State {
}
_launchURL(String url) async {
- if (await canLaunch(url)) {
- await launch(url);
+ final uri = Uri.parse(url);
+ if (await canLaunchUrl(uri)) {
+ await launchUrl(uri);
} else {
throw 'Could not launch $url';
}
diff --git a/frontend/app_flowy/lib/workspace/application/app/app_bloc.dart b/frontend/app_flowy/lib/workspace/application/app/app_bloc.dart
index 663e630a6a..e5eabe98e4 100644
--- a/frontend/app_flowy/lib/workspace/application/app/app_bloc.dart
+++ b/frontend/app_flowy/lib/workspace/application/app/app_bloc.dart
@@ -1,3 +1,5 @@
+import 'dart:collection';
+
import 'package:app_flowy/plugin/plugin.dart';
import 'package:app_flowy/startup/startup.dart';
import 'package:app_flowy/workspace/application/app/app_listener.dart';
diff --git a/frontend/app_flowy/lib/workspace/application/grid/cell/cell_service/cell_service.dart b/frontend/app_flowy/lib/workspace/application/grid/cell/cell_service/cell_service.dart
index 07be14efa2..68f8eada78 100644
--- a/frontend/app_flowy/lib/workspace/application/grid/cell/cell_service/cell_service.dart
+++ b/frontend/app_flowy/lib/workspace/application/grid/cell/cell_service/cell_service.dart
@@ -10,13 +10,13 @@ import 'package:flowy_sdk/protobuf/flowy-grid-data-model/grid.pb.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/cell_entities.pb.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/date_type_option.pb.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/selection_type_option.pb.dart';
+import 'package:flowy_sdk/protobuf/flowy-grid/url_type_option.pb.dart';
import 'package:flutter/foundation.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
-
import 'package:app_flowy/workspace/application/grid/cell/cell_listener.dart';
import 'package:app_flowy/workspace/application/grid/cell/select_option_service.dart';
import 'package:app_flowy/workspace/application/grid/field/field_service.dart';
-
+import 'dart:convert' show utf8;
part 'cell_service.freezed.dart';
part 'data_loader.dart';
part 'context_builder.dart';
diff --git a/frontend/app_flowy/lib/workspace/application/grid/cell/cell_service/context_builder.dart b/frontend/app_flowy/lib/workspace/application/grid/cell/cell_service/context_builder.dart
index ed191c7d60..e4141c3e16 100644
--- a/frontend/app_flowy/lib/workspace/application/grid/cell/cell_service/context_builder.dart
+++ b/frontend/app_flowy/lib/workspace/application/grid/cell/cell_service/context_builder.dart
@@ -1,8 +1,9 @@
part of 'cell_service.dart';
-typedef GridCellContext = _GridCellContext;
+typedef GridCellContext = _GridCellContext;
typedef GridSelectOptionCellContext = _GridCellContext;
typedef GridDateCellContext = _GridCellContext;
+typedef GridURLCellContext = _GridCellContext;
class GridCellContextBuilder {
final GridCellCache _cellCache;
@@ -16,26 +17,33 @@ class GridCellContextBuilder {
_GridCellContext build() {
switch (_gridCell.field.fieldType) {
case FieldType.Checkbox:
+ final cellDataLoader = GridCellDataLoader(
+ gridCell: _gridCell,
+ parser: StringCellDataParser(),
+ );
return GridCellContext(
gridCell: _gridCell,
cellCache: _cellCache,
- cellDataLoader: GridCellDataLoader(gridCell: _gridCell),
+ cellDataLoader: cellDataLoader,
cellDataPersistence: CellDataPersistence(gridCell: _gridCell),
);
case FieldType.DateTime:
+ final cellDataLoader = GridCellDataLoader(
+ gridCell: _gridCell,
+ parser: DateCellDataParser(),
+ );
+
return GridDateCellContext(
gridCell: _gridCell,
cellCache: _cellCache,
- cellDataLoader: DateCellDataLoader(gridCell: _gridCell),
+ cellDataLoader: cellDataLoader,
cellDataPersistence: DateCellDataPersistence(gridCell: _gridCell),
);
case FieldType.Number:
final cellDataLoader = GridCellDataLoader(
gridCell: _gridCell,
- config: const GridCellDataConfig(
- reloadOnCellChanged: true,
- reloadOnFieldChanged: true,
- ),
+ parser: StringCellDataParser(),
+ config: const GridCellDataConfig(reloadOnCellChanged: true, reloadOnFieldChanged: true),
);
return GridCellContext(
gridCell: _gridCell,
@@ -44,26 +52,49 @@ class GridCellContextBuilder {
cellDataPersistence: CellDataPersistence(gridCell: _gridCell),
);
case FieldType.RichText:
+ final cellDataLoader = GridCellDataLoader(
+ gridCell: _gridCell,
+ parser: StringCellDataParser(),
+ );
return GridCellContext(
gridCell: _gridCell,
cellCache: _cellCache,
- cellDataLoader: GridCellDataLoader(gridCell: _gridCell),
+ cellDataLoader: cellDataLoader,
cellDataPersistence: CellDataPersistence(gridCell: _gridCell),
);
case FieldType.MultiSelect:
case FieldType.SingleSelect:
+ final cellDataLoader = GridCellDataLoader(
+ gridCell: _gridCell,
+ parser: SelectOptionCellDataParser(),
+ config: const GridCellDataConfig(reloadOnFieldChanged: true),
+ );
+
return GridSelectOptionCellContext(
gridCell: _gridCell,
cellCache: _cellCache,
- cellDataLoader: SelectOptionCellDataLoader(gridCell: _gridCell),
+ cellDataLoader: cellDataLoader,
+ cellDataPersistence: CellDataPersistence(gridCell: _gridCell),
+ );
+
+ case FieldType.URL:
+ final cellDataLoader = GridCellDataLoader(
+ gridCell: _gridCell,
+ parser: URLCellDataParser(),
+ );
+ return GridURLCellContext(
+ gridCell: _gridCell,
+ cellCache: _cellCache,
+ cellDataLoader: cellDataLoader,
cellDataPersistence: CellDataPersistence(gridCell: _gridCell),
);
- default:
- throw UnimplementedError;
}
+ throw UnimplementedError;
}
}
+// T: the type of the CellData
+// D: the type of the data that will be save to disk
// ignore: must_be_immutable
class _GridCellContext extends Equatable {
final GridCell gridCell;
@@ -77,7 +108,8 @@ class _GridCellContext extends Equatable {
late final ValueNotifier _cellDataNotifier;
bool isListening = false;
VoidCallback? _onFieldChangedFn;
- Timer? _delayOperation;
+ Timer? _loadDataOperation;
+ Timer? _saveDataOperation;
_GridCellContext({
required this.gridCell,
@@ -107,7 +139,7 @@ class _GridCellContext extends Equatable {
FieldType get fieldType => gridCell.field.fieldType;
- VoidCallback? startListening({required void Function(T) onCellChanged}) {
+ VoidCallback? startListening({required void Function(T?) onCellChanged}) {
if (isListening) {
Log.error("Already started. It seems like you should call clone first");
return null;
@@ -131,10 +163,7 @@ class _GridCellContext extends Equatable {
}
onCellChangedFn() {
- final value = _cellDataNotifier.value;
- if (value is T) {
- onCellChanged(value);
- }
+ onCellChanged(_cellDataNotifier.value);
if (cellDataLoader.config.reloadOnCellChanged) {
_loadData();
@@ -149,9 +178,9 @@ class _GridCellContext extends Equatable {
_cellDataNotifier.removeListener(fn);
}
- T? getCellData() {
+ T? getCellData({bool loadIfNoCache = true}) {
final data = cellCache.get(_cacheKey);
- if (data == null) {
+ if (data == null && loadIfNoCache) {
_loadData();
}
return data;
@@ -161,13 +190,26 @@ class _GridCellContext extends Equatable {
return _fieldService.getFieldTypeOptionData(fieldType: fieldType);
}
- Future |