mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
chore: support open url with https scheme
This commit is contained in:
parent
ae0f71b5ee
commit
4a9627b31d
@ -108,7 +108,8 @@ class _GridCellContext<T, D> extends Equatable {
|
||||
late final ValueNotifier<T?> _cellDataNotifier;
|
||||
bool isListening = false;
|
||||
VoidCallback? _onFieldChangedFn;
|
||||
Timer? _delayOperation;
|
||||
Timer? _loadDataOperation;
|
||||
Timer? _saveDataOperation;
|
||||
|
||||
_GridCellContext({
|
||||
required this.gridCell,
|
||||
@ -138,7 +139,7 @@ class _GridCellContext<T, D> 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;
|
||||
@ -162,7 +163,7 @@ class _GridCellContext<T, D> extends Equatable {
|
||||
}
|
||||
|
||||
onCellChangedFn() {
|
||||
onCellChanged(_cellDataNotifier.value as T);
|
||||
onCellChanged(_cellDataNotifier.value);
|
||||
|
||||
if (cellDataLoader.config.reloadOnCellChanged) {
|
||||
_loadData();
|
||||
@ -189,13 +190,26 @@ class _GridCellContext<T, D> extends Equatable {
|
||||
return _fieldService.getFieldTypeOptionData(fieldType: fieldType);
|
||||
}
|
||||
|
||||
Future<Option<FlowyError>> saveCellData(D data) {
|
||||
return cellDataPersistence.save(data);
|
||||
void saveCellData(D data, {bool deduplicate = false, void Function(Option<FlowyError>)? resultCallback}) async {
|
||||
if (deduplicate) {
|
||||
_loadDataOperation?.cancel();
|
||||
_loadDataOperation = Timer(const Duration(milliseconds: 300), () async {
|
||||
final result = await cellDataPersistence.save(data);
|
||||
if (resultCallback != null) {
|
||||
resultCallback(result);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
final result = await cellDataPersistence.save(data);
|
||||
if (resultCallback != null) {
|
||||
resultCallback(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _loadData() {
|
||||
_delayOperation?.cancel();
|
||||
_delayOperation = Timer(const Duration(milliseconds: 10), () {
|
||||
_loadDataOperation?.cancel();
|
||||
_loadDataOperation = Timer(const Duration(milliseconds: 10), () {
|
||||
cellDataLoader.loadData().then((data) {
|
||||
_cellDataNotifier.value = data;
|
||||
cellCache.insert(GridCellCacheData(key: _cacheKey, object: data));
|
||||
@ -204,7 +218,8 @@ class _GridCellContext<T, D> extends Equatable {
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
_delayOperation?.cancel();
|
||||
_loadDataOperation?.cancel();
|
||||
_saveDataOperation?.cancel();
|
||||
|
||||
if (_onFieldChangedFn != null) {
|
||||
cellCache.removeFieldListener(_cacheKey, _onFieldChangedFn!);
|
||||
|
@ -58,7 +58,7 @@ class CheckboxCellBloc extends Bloc<CheckboxCellEvent, CheckboxCellState> {
|
||||
class CheckboxCellEvent with _$CheckboxCellEvent {
|
||||
const factory CheckboxCellEvent.initial() = _Initial;
|
||||
const factory CheckboxCellEvent.select() = _Selected;
|
||||
const factory CheckboxCellEvent.didReceiveCellUpdate(String cellData) = _DidReceiveCellUpdate;
|
||||
const factory CheckboxCellEvent.didReceiveCellUpdate(String? cellData) = _DidReceiveCellUpdate;
|
||||
}
|
||||
|
||||
@freezed
|
||||
|
@ -37,7 +37,7 @@ class DateCalBloc extends Bloc<DateCalEvent, DateCalState> {
|
||||
setFocusedDay: (focusedDay) {
|
||||
emit(state.copyWith(focusedDay: focusedDay));
|
||||
},
|
||||
didReceiveCellUpdate: (DateCellData cellData) {
|
||||
didReceiveCellUpdate: (DateCellData? cellData) {
|
||||
final dateData = dateDataFromCellData(cellData);
|
||||
final time = dateData.foldRight("", (dateData, previous) => dateData.time);
|
||||
emit(state.copyWith(dateData: dateData, time: time));
|
||||
@ -83,25 +83,26 @@ class DateCalBloc extends Bloc<DateCalEvent, DateCalState> {
|
||||
return;
|
||||
}
|
||||
|
||||
final result = await cellContext.saveCellData(newDateData);
|
||||
result.fold(
|
||||
() => emit(state.copyWith(
|
||||
dateData: Some(newDateData),
|
||||
timeFormatError: none(),
|
||||
)),
|
||||
(err) {
|
||||
switch (ErrorCode.valueOf(err.code)!) {
|
||||
case ErrorCode.InvalidDateTimeFormat:
|
||||
emit(state.copyWith(
|
||||
dateData: Some(newDateData),
|
||||
timeFormatError: Some(timeFormatPrompt(err)),
|
||||
));
|
||||
break;
|
||||
default:
|
||||
Log.error(err);
|
||||
}
|
||||
},
|
||||
);
|
||||
cellContext.saveCellData(newDateData, resultCallback: (result) {
|
||||
result.fold(
|
||||
() => emit(state.copyWith(
|
||||
dateData: Some(newDateData),
|
||||
timeFormatError: none(),
|
||||
)),
|
||||
(err) {
|
||||
switch (ErrorCode.valueOf(err.code)!) {
|
||||
case ErrorCode.InvalidDateTimeFormat:
|
||||
emit(state.copyWith(
|
||||
dateData: Some(newDateData),
|
||||
timeFormatError: Some(timeFormatPrompt(err)),
|
||||
));
|
||||
break;
|
||||
default:
|
||||
Log.error(err);
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
String timeFormatPrompt(FlowyError error) {
|
||||
@ -183,7 +184,7 @@ class DateCalEvent with _$DateCalEvent {
|
||||
const factory DateCalEvent.setDateFormat(DateFormat dateFormat) = _DateFormat;
|
||||
const factory DateCalEvent.setIncludeTime(bool includeTime) = _IncludeTime;
|
||||
const factory DateCalEvent.setTime(String time) = _Time;
|
||||
const factory DateCalEvent.didReceiveCellUpdate(DateCellData data) = _DidReceiveCellUpdate;
|
||||
const factory DateCalEvent.didReceiveCellUpdate(DateCellData? data) = _DidReceiveCellUpdate;
|
||||
}
|
||||
|
||||
@freezed
|
||||
|
@ -16,7 +16,13 @@ class DateCellBloc extends Bloc<DateCellEvent, DateCellState> {
|
||||
(event, emit) async {
|
||||
event.when(
|
||||
initial: () => _startListening(),
|
||||
didReceiveCellUpdate: (DateCellData value) => emit(state.copyWith(data: Some(value))),
|
||||
didReceiveCellUpdate: (DateCellData? cellData) {
|
||||
if (cellData != null) {
|
||||
emit(state.copyWith(data: Some(cellData)));
|
||||
} else {
|
||||
emit(state.copyWith(data: none()));
|
||||
}
|
||||
},
|
||||
didReceiveFieldUpdate: (Field value) => emit(state.copyWith(field: value)),
|
||||
);
|
||||
},
|
||||
@ -47,7 +53,7 @@ class DateCellBloc extends Bloc<DateCellEvent, DateCellState> {
|
||||
@freezed
|
||||
class DateCellEvent with _$DateCellEvent {
|
||||
const factory DateCellEvent.initial() = _InitialCell;
|
||||
const factory DateCellEvent.didReceiveCellUpdate(DateCellData data) = _DidReceiveCellUpdate;
|
||||
const factory DateCellEvent.didReceiveCellUpdate(DateCellData? data) = _DidReceiveCellUpdate;
|
||||
const factory DateCellEvent.didReceiveFieldUpdate(Field field) = _DidReceiveFieldUpdate;
|
||||
}
|
||||
|
||||
|
@ -19,7 +19,7 @@ class NumberCellBloc extends Bloc<NumberCellEvent, NumberCellState> {
|
||||
_startListening();
|
||||
},
|
||||
didReceiveCellUpdate: (_DidReceiveCellUpdate value) {
|
||||
emit(state.copyWith(content: value.cellContent));
|
||||
emit(state.copyWith(content: value.cellContent ?? ""));
|
||||
},
|
||||
updateCell: (_UpdateCell value) async {
|
||||
await _updateCellValue(value, emit);
|
||||
@ -58,7 +58,7 @@ class NumberCellBloc extends Bloc<NumberCellEvent, NumberCellState> {
|
||||
class NumberCellEvent with _$NumberCellEvent {
|
||||
const factory NumberCellEvent.initial() = _Initial;
|
||||
const factory NumberCellEvent.updateCell(String text) = _UpdateCell;
|
||||
const factory NumberCellEvent.didReceiveCellUpdate(String cellContent) = _DidReceiveCellUpdate;
|
||||
const factory NumberCellEvent.didReceiveCellUpdate(String? cellContent) = _DidReceiveCellUpdate;
|
||||
}
|
||||
|
||||
@freezed
|
||||
|
@ -44,7 +44,7 @@ class SelectOptionCellBloc extends Bloc<SelectOptionCellEvent, SelectOptionCellS
|
||||
onCellChanged: ((selectOptionContext) {
|
||||
if (!isClosed) {
|
||||
add(SelectOptionCellEvent.didReceiveOptions(
|
||||
selectOptionContext.selectOptions,
|
||||
selectOptionContext?.selectOptions ?? [],
|
||||
));
|
||||
}
|
||||
}),
|
||||
|
@ -43,7 +43,7 @@ class TextCellBloc extends Bloc<TextCellEvent, TextCellState> {
|
||||
_onCellChangedFn = cellContext.startListening(
|
||||
onCellChanged: ((cellContent) {
|
||||
if (!isClosed) {
|
||||
add(TextCellEvent.didReceiveCellUpdate(cellContent));
|
||||
add(TextCellEvent.didReceiveCellUpdate(cellContent ?? ""));
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
@ -18,12 +18,11 @@ class URLCellBloc extends Bloc<URLCellEvent, URLCellState> {
|
||||
initial: () {
|
||||
_startListening();
|
||||
},
|
||||
updateText: (text) {
|
||||
cellContext.saveCellData(text);
|
||||
emit(state.copyWith(content: text));
|
||||
},
|
||||
didReceiveCellUpdate: (cellData) {
|
||||
emit(state.copyWith(content: cellData.content, url: cellData.url));
|
||||
emit(state.copyWith(
|
||||
content: cellData?.content ?? "",
|
||||
url: cellData?.url ?? "",
|
||||
));
|
||||
},
|
||||
);
|
||||
},
|
||||
@ -54,8 +53,7 @@ class URLCellBloc extends Bloc<URLCellEvent, URLCellState> {
|
||||
@freezed
|
||||
class URLCellEvent with _$URLCellEvent {
|
||||
const factory URLCellEvent.initial() = _InitialCell;
|
||||
const factory URLCellEvent.didReceiveCellUpdate(URLCellData cell) = _DidReceiveCellUpdate;
|
||||
const factory URLCellEvent.updateText(String text) = _UpdateText;
|
||||
const factory URLCellEvent.didReceiveCellUpdate(URLCellData? cell) = _DidReceiveCellUpdate;
|
||||
}
|
||||
|
||||
@freezed
|
||||
|
@ -0,0 +1,73 @@
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/url_type_option.pb.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'dart:async';
|
||||
import 'cell_service/cell_service.dart';
|
||||
|
||||
part 'url_cell_editor_bloc.freezed.dart';
|
||||
|
||||
class URLCellEditorBloc extends Bloc<URLCellEditorEvent, URLCellEditorState> {
|
||||
final GridURLCellContext cellContext;
|
||||
void Function()? _onCellChangedFn;
|
||||
URLCellEditorBloc({
|
||||
required this.cellContext,
|
||||
}) : super(URLCellEditorState.initial(cellContext)) {
|
||||
on<URLCellEditorEvent>(
|
||||
(event, emit) async {
|
||||
event.when(
|
||||
initial: () {
|
||||
_startListening();
|
||||
},
|
||||
updateText: (text) {
|
||||
cellContext.saveCellData(text, deduplicate: true);
|
||||
emit(state.copyWith(content: text));
|
||||
},
|
||||
didReceiveCellUpdate: (cellData) {
|
||||
emit(state.copyWith(content: cellData?.content ?? ""));
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
if (_onCellChangedFn != null) {
|
||||
cellContext.removeListener(_onCellChangedFn!);
|
||||
_onCellChangedFn = null;
|
||||
}
|
||||
cellContext.dispose();
|
||||
return super.close();
|
||||
}
|
||||
|
||||
void _startListening() {
|
||||
_onCellChangedFn = cellContext.startListening(
|
||||
onCellChanged: ((cellData) {
|
||||
if (!isClosed) {
|
||||
add(URLCellEditorEvent.didReceiveCellUpdate(cellData));
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
class URLCellEditorEvent with _$URLCellEditorEvent {
|
||||
const factory URLCellEditorEvent.initial() = _InitialCell;
|
||||
const factory URLCellEditorEvent.didReceiveCellUpdate(URLCellData? cell) = _DidReceiveCellUpdate;
|
||||
const factory URLCellEditorEvent.updateText(String text) = _UpdateText;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class URLCellEditorState with _$URLCellEditorState {
|
||||
const factory URLCellEditorState({
|
||||
required String content,
|
||||
}) = _URLCellEditorState;
|
||||
|
||||
factory URLCellEditorState.initial(GridURLCellContext context) {
|
||||
final cellData = context.getCellData();
|
||||
return URLCellEditorState(
|
||||
content: cellData?.content ?? "",
|
||||
);
|
||||
}
|
||||
}
|
@ -13,7 +13,7 @@ import 'date_cell/date_cell.dart';
|
||||
import 'number_cell.dart';
|
||||
import 'select_option_cell/select_option_cell.dart';
|
||||
import 'text_cell.dart';
|
||||
import 'url_cell.dart';
|
||||
import 'url_cell/url_cell.dart';
|
||||
|
||||
GridCellWidget buildGridCellWidget(GridCell gridCell, GridCellCache cellCache, {GridCellStyle? style}) {
|
||||
final key = ValueKey(gridCell.cellId());
|
||||
@ -35,7 +35,6 @@ GridCellWidget buildGridCellWidget(GridCell gridCell, GridCellCache cellCache, {
|
||||
return GridTextCell(cellContextBuilder: cellContextBuilder, style: style, key: key);
|
||||
case FieldType.URL:
|
||||
return GridURLCell(cellContextBuilder: cellContextBuilder, style: style, key: key);
|
||||
|
||||
}
|
||||
throw UnimplementedError;
|
||||
}
|
||||
@ -151,7 +150,7 @@ class CellContainer extends StatelessWidget {
|
||||
});
|
||||
|
||||
if (expander != null) {
|
||||
container = _CellEnterRegion(child: container, expander: expander!);
|
||||
container = CellEnterRegion(child: container, expander: expander!);
|
||||
}
|
||||
|
||||
return GestureDetector(
|
||||
@ -181,10 +180,10 @@ class CellContainer extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _CellEnterRegion extends StatelessWidget {
|
||||
class CellEnterRegion extends StatelessWidget {
|
||||
final Widget child;
|
||||
final Widget expander;
|
||||
const _CellEnterRegion({required this.child, required this.expander, Key? key}) : super(key: key);
|
||||
const CellEnterRegion({required this.child, required this.expander, Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
@ -1,127 +0,0 @@
|
||||
import 'dart:async';
|
||||
import 'package:app_flowy/workspace/application/grid/cell/url_cell_bloc.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:app_flowy/workspace/application/grid/prelude.dart';
|
||||
import 'cell_builder.dart';
|
||||
|
||||
class GridURLCellStyle extends GridCellStyle {
|
||||
String? placeholder;
|
||||
|
||||
GridURLCellStyle({
|
||||
this.placeholder,
|
||||
});
|
||||
}
|
||||
|
||||
class GridURLCell extends StatefulWidget with GridCellWidget {
|
||||
final GridCellContextBuilder cellContextBuilder;
|
||||
late final GridURLCellStyle? cellStyle;
|
||||
GridURLCell({
|
||||
required this.cellContextBuilder,
|
||||
GridCellStyle? style,
|
||||
Key? key,
|
||||
}) : super(key: key) {
|
||||
if (style != null) {
|
||||
cellStyle = (style as GridURLCellStyle);
|
||||
} else {
|
||||
cellStyle = null;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
State<GridURLCell> createState() => _GridURLCellState();
|
||||
}
|
||||
|
||||
class _GridURLCellState extends State<GridURLCell> {
|
||||
late URLCellBloc _cellBloc;
|
||||
late TextEditingController _controller;
|
||||
late CellSingleFocusNode _focusNode;
|
||||
Timer? _delayOperation;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
final cellContext = widget.cellContextBuilder.build() as GridURLCellContext;
|
||||
_cellBloc = URLCellBloc(cellContext: cellContext);
|
||||
_cellBloc.add(const URLCellEvent.initial());
|
||||
_controller = TextEditingController(text: _cellBloc.state.content);
|
||||
_focusNode = CellSingleFocusNode();
|
||||
|
||||
_listenFocusNode();
|
||||
_listenRequestFocus(context);
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider.value(
|
||||
value: _cellBloc,
|
||||
child: BlocListener<URLCellBloc, URLCellState>(
|
||||
listener: (context, state) {
|
||||
if (_controller.text != state.content) {
|
||||
_controller.text = state.content;
|
||||
}
|
||||
},
|
||||
child: TextField(
|
||||
controller: _controller,
|
||||
focusNode: _focusNode,
|
||||
onChanged: (value) => focusChanged(),
|
||||
onEditingComplete: () => _focusNode.unfocus(),
|
||||
maxLines: null,
|
||||
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
|
||||
decoration: InputDecoration(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
border: InputBorder.none,
|
||||
hintText: widget.cellStyle?.placeholder,
|
||||
isDense: true,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> dispose() async {
|
||||
widget.requestFocus.removeAllListener();
|
||||
_delayOperation?.cancel();
|
||||
_cellBloc.close();
|
||||
_focusNode.removeSingleListener();
|
||||
_focusNode.dispose();
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant GridURLCell oldWidget) {
|
||||
if (oldWidget != widget) {
|
||||
_listenFocusNode();
|
||||
}
|
||||
super.didUpdateWidget(oldWidget);
|
||||
}
|
||||
|
||||
void _listenFocusNode() {
|
||||
widget.onFocus.value = _focusNode.hasFocus;
|
||||
_focusNode.setSingleListener(() {
|
||||
widget.onFocus.value = _focusNode.hasFocus;
|
||||
focusChanged();
|
||||
});
|
||||
}
|
||||
|
||||
void _listenRequestFocus(BuildContext context) {
|
||||
widget.requestFocus.addListener(() {
|
||||
if (_focusNode.hasFocus == false && _focusNode.canRequestFocus) {
|
||||
FocusScope.of(context).requestFocus(_focusNode);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> focusChanged() async {
|
||||
if (mounted) {
|
||||
_delayOperation?.cancel();
|
||||
_delayOperation = Timer(const Duration(milliseconds: 300), () {
|
||||
if (_cellBloc.isClosed == false && _controller.text != _cellBloc.state.content) {
|
||||
_cellBloc.add(URLCellEvent.updateText(_controller.text));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,96 @@
|
||||
import 'package:app_flowy/workspace/application/grid/cell/cell_service/cell_service.dart';
|
||||
import 'package:app_flowy/workspace/application/grid/cell/url_cell_editor_bloc.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class URLCellEditor extends StatefulWidget {
|
||||
final GridURLCellContext cellContext;
|
||||
const URLCellEditor({required this.cellContext, Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<URLCellEditor> createState() => _URLCellEditorState();
|
||||
|
||||
static void show(
|
||||
BuildContext context,
|
||||
GridURLCellContext cellContext,
|
||||
) {
|
||||
FlowyOverlay.of(context).remove(identifier());
|
||||
final editor = URLCellEditor(
|
||||
cellContext: cellContext,
|
||||
);
|
||||
|
||||
//
|
||||
FlowyOverlay.of(context).insertWithAnchor(
|
||||
widget: OverlayContainer(
|
||||
child: SizedBox(width: 200, child: editor),
|
||||
constraints: BoxConstraints.loose(const Size(300, 160)),
|
||||
),
|
||||
identifier: URLCellEditor.identifier(),
|
||||
anchorContext: context,
|
||||
anchorDirection: AnchorDirection.bottomWithCenterAligned,
|
||||
);
|
||||
}
|
||||
|
||||
static String identifier() {
|
||||
return (URLCellEditor).toString();
|
||||
}
|
||||
}
|
||||
|
||||
class _URLCellEditorState extends State<URLCellEditor> {
|
||||
late URLCellEditorBloc _cellBloc;
|
||||
late TextEditingController _controller;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_cellBloc = URLCellEditorBloc(cellContext: widget.cellContext);
|
||||
_cellBloc.add(const URLCellEditorEvent.initial());
|
||||
_controller = TextEditingController(text: _cellBloc.state.content);
|
||||
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider.value(
|
||||
value: _cellBloc,
|
||||
child: BlocListener<URLCellEditorBloc, URLCellEditorState>(
|
||||
listener: (context, state) {
|
||||
if (_controller.text != state.content) {
|
||||
_controller.text = state.content;
|
||||
}
|
||||
},
|
||||
child: TextField(
|
||||
autofocus: true,
|
||||
controller: _controller,
|
||||
onChanged: (value) => focusChanged(),
|
||||
maxLines: null,
|
||||
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
|
||||
decoration: const InputDecoration(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
border: InputBorder.none,
|
||||
hintText: "",
|
||||
isDense: true,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> dispose() async {
|
||||
_cellBloc.close();
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> focusChanged() async {
|
||||
if (mounted) {
|
||||
if (_cellBloc.isClosed == false && _controller.text != _cellBloc.state.content) {
|
||||
_cellBloc.add(URLCellEditorEvent.updateText(_controller.text));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,131 @@
|
||||
import 'dart:async';
|
||||
import 'package:app_flowy/workspace/application/grid/cell/url_cell_bloc.dart';
|
||||
import 'package:flowy_infra/image.dart';
|
||||
import 'package:flowy_infra/theme.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/icon_button.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:app_flowy/workspace/application/grid/prelude.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import '../cell_builder.dart';
|
||||
import 'cell_editor.dart';
|
||||
|
||||
class GridURLCellStyle extends GridCellStyle {
|
||||
String? placeholder;
|
||||
|
||||
GridURLCellStyle({
|
||||
this.placeholder,
|
||||
});
|
||||
}
|
||||
|
||||
class GridURLCell extends StatefulWidget with GridCellWidget {
|
||||
final GridCellContextBuilder cellContextBuilder;
|
||||
late final GridURLCellStyle? cellStyle;
|
||||
GridURLCell({
|
||||
required this.cellContextBuilder,
|
||||
GridCellStyle? style,
|
||||
Key? key,
|
||||
}) : super(key: key) {
|
||||
if (style != null) {
|
||||
cellStyle = (style as GridURLCellStyle);
|
||||
} else {
|
||||
cellStyle = null;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
State<GridURLCell> createState() => _GridURLCellState();
|
||||
}
|
||||
|
||||
class _GridURLCellState extends State<GridURLCell> {
|
||||
late URLCellBloc _cellBloc;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
final cellContext = widget.cellContextBuilder.build() as GridURLCellContext;
|
||||
_cellBloc = URLCellBloc(cellContext: cellContext);
|
||||
_cellBloc.add(const URLCellEvent.initial());
|
||||
_listenRequestFocus(context);
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = context.watch<AppTheme>();
|
||||
return BlocProvider.value(
|
||||
value: _cellBloc,
|
||||
child: BlocBuilder<URLCellBloc, URLCellState>(
|
||||
builder: (context, state) {
|
||||
final richText = RichText(
|
||||
textAlign: TextAlign.left,
|
||||
text: TextSpan(
|
||||
text: state.content,
|
||||
style: TextStyle(
|
||||
color: theme.main2,
|
||||
fontSize: 14,
|
||||
decoration: TextDecoration.underline,
|
||||
),
|
||||
recognizer: _tapGesture(context),
|
||||
),
|
||||
);
|
||||
|
||||
return CellEnterRegion(
|
||||
child: Align(alignment: Alignment.centerLeft, child: richText),
|
||||
expander: _EditCellIndicator(onTap: () {}),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> dispose() async {
|
||||
widget.requestFocus.removeAllListener();
|
||||
_cellBloc.close();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
TapGestureRecognizer _tapGesture(BuildContext context) {
|
||||
final gesture = TapGestureRecognizer();
|
||||
gesture.onTap = () async {
|
||||
final url = context.read<URLCellBloc>().state.url;
|
||||
await _openUrlOrEdit(url);
|
||||
};
|
||||
return gesture;
|
||||
}
|
||||
|
||||
Future<void> _openUrlOrEdit(String url) async {
|
||||
final uri = Uri.parse(url);
|
||||
if (url.isNotEmpty && await canLaunchUrl(uri)) {
|
||||
await launchUrl(uri);
|
||||
} else {
|
||||
final cellContext = widget.cellContextBuilder.build() as GridURLCellContext;
|
||||
URLCellEditor.show(context, cellContext);
|
||||
}
|
||||
}
|
||||
|
||||
void _listenRequestFocus(BuildContext context) {
|
||||
widget.requestFocus.addListener(() {
|
||||
_openUrlOrEdit(_cellBloc.state.url);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class _EditCellIndicator extends StatelessWidget {
|
||||
final VoidCallback onTap;
|
||||
const _EditCellIndicator({required this.onTap, Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = context.watch<AppTheme>();
|
||||
return FlowyIconButton(
|
||||
width: 26,
|
||||
onPressed: onTap,
|
||||
hoverColor: theme.hover,
|
||||
radius: BorderRadius.circular(4),
|
||||
iconPadding: const EdgeInsets.all(5),
|
||||
icon: svgWidget("editor/edit", color: theme.iconColor),
|
||||
);
|
||||
}
|
||||
}
|
@ -209,9 +209,10 @@ class _CellExpander extends StatelessWidget {
|
||||
return FittedBox(
|
||||
fit: BoxFit.contain,
|
||||
child: FlowyIconButton(
|
||||
width: 26,
|
||||
onPressed: onExpand,
|
||||
iconPadding: const EdgeInsets.fromLTRB(6, 6, 6, 6),
|
||||
fillColor: theme.surface,
|
||||
iconPadding: const EdgeInsets.all(5),
|
||||
radius: BorderRadius.circular(4),
|
||||
icon: svgWidget("grid/expander", color: theme.main1),
|
||||
),
|
||||
);
|
||||
|
@ -4,7 +4,7 @@ import 'package:app_flowy/workspace/application/grid/row/row_detail_bloc.dart';
|
||||
import 'package:app_flowy/workspace/application/grid/row/row_service.dart';
|
||||
import 'package:app_flowy/workspace/presentation/plugins/grid/src/layout/sizes.dart';
|
||||
import 'package:app_flowy/workspace/presentation/plugins/grid/src/widgets/cell/prelude.dart';
|
||||
import 'package:app_flowy/workspace/presentation/plugins/grid/src/widgets/cell/url_cell.dart';
|
||||
import 'package:app_flowy/workspace/presentation/plugins/grid/src/widgets/cell/url_cell/url_cell.dart';
|
||||
import 'package:app_flowy/workspace/presentation/plugins/grid/src/widgets/header/field_cell.dart';
|
||||
import 'package:app_flowy/workspace/presentation/plugins/grid/src/widgets/header/field_editor.dart';
|
||||
import 'package:flowy_infra/image.dart';
|
||||
|
1
frontend/rust-lib/Cargo.lock
generated
1
frontend/rust-lib/Cargo.lock
generated
@ -954,6 +954,7 @@ dependencies = [
|
||||
"strum_macros",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -36,6 +36,7 @@ serde_json = {version = "1.0"}
|
||||
serde_repr = "0.1"
|
||||
indexmap = {version = "1.8.1", features = ["serde"]}
|
||||
fancy-regex = "0.10.0"
|
||||
url = { version = "2"}
|
||||
|
||||
[dev-dependencies]
|
||||
flowy-test = { path = "../flowy-test" }
|
||||
|
@ -207,8 +207,6 @@ impl CellDataOperation<String, String> for MultiSelectTypeOption {
|
||||
return Ok(DecodedCellData::default());
|
||||
}
|
||||
|
||||
tracing::info!("😁{}", self.options.len());
|
||||
|
||||
let encoded_data = encoded_data.into();
|
||||
let select_options = select_option_ids(encoded_data)
|
||||
.into_iter()
|
||||
|
@ -62,7 +62,19 @@ impl CellDataOperation<EncodedCellData<URLCellData>, String> for URLTypeOption {
|
||||
};
|
||||
|
||||
if let Ok(Some(m)) = URL_REGEX.find(&changeset) {
|
||||
cell_data.url = m.as_str().to_string();
|
||||
// Only support https scheme by now
|
||||
match url::Url::parse(m.as_str()) {
|
||||
Ok(url) => {
|
||||
if url.scheme() == "https" {
|
||||
cell_data.url = url.into();
|
||||
} else {
|
||||
cell_data.url = format!("https://{}", m.as_str());
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
cell_data.url = format!("https://{}", m.as_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cell_data.to_json()
|
||||
@ -132,7 +144,7 @@ mod tests {
|
||||
&field_type,
|
||||
&field_meta,
|
||||
"AppFlowy website - https://www.appflowy.io",
|
||||
"https://www.appflowy.io",
|
||||
"https://www.appflowy.io/",
|
||||
);
|
||||
|
||||
assert_changeset(
|
||||
@ -141,7 +153,7 @@ mod tests {
|
||||
&field_type,
|
||||
&field_meta,
|
||||
"AppFlowy website appflowy.io",
|
||||
"appflowy.io",
|
||||
"https://appflowy.io",
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -167,7 +167,6 @@ pub fn decode_cell_data<T: Into<String>>(
|
||||
field_meta: &FieldMeta,
|
||||
) -> FlowyResult<DecodedCellData> {
|
||||
let encoded_data = encoded_data.into();
|
||||
tracing::info!("😁{:?}", field_meta.type_options);
|
||||
let get_cell_data = || {
|
||||
let data = match t_field_type {
|
||||
FieldType::RichText => field_meta
|
||||
|
Loading…
Reference in New Issue
Block a user