mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
chore: remove flutter quill (#1426)
This commit is contained in:
parent
cdf6f1b38a
commit
42c2c4738a
@ -1,74 +0,0 @@
|
|||||||
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/material.dart';
|
|
||||||
import 'package:flutter_quill/flutter_quill.dart';
|
|
||||||
|
|
||||||
class StyleWidgetBuilder {
|
|
||||||
static QuillCheckboxBuilder checkbox(AppTheme theme) {
|
|
||||||
return EditorCheckboxBuilder(theme);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class EditorCheckboxBuilder extends QuillCheckboxBuilder {
|
|
||||||
final AppTheme theme;
|
|
||||||
|
|
||||||
EditorCheckboxBuilder(this.theme);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(
|
|
||||||
{required BuildContext context,
|
|
||||||
required bool isChecked,
|
|
||||||
required ValueChanged<bool> onChanged}) {
|
|
||||||
return FlowyEditorCheckbox(
|
|
||||||
theme: theme,
|
|
||||||
isChecked: isChecked,
|
|
||||||
onChanged: onChanged,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class FlowyEditorCheckbox extends StatefulWidget {
|
|
||||||
final bool isChecked;
|
|
||||||
final ValueChanged<bool> onChanged;
|
|
||||||
final AppTheme theme;
|
|
||||||
const FlowyEditorCheckbox({
|
|
||||||
required this.theme,
|
|
||||||
required this.isChecked,
|
|
||||||
required this.onChanged,
|
|
||||||
Key? key,
|
|
||||||
}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
FlowyEditorCheckboxState createState() => FlowyEditorCheckboxState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class FlowyEditorCheckboxState extends State<FlowyEditorCheckbox> {
|
|
||||||
late bool isChecked;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
isChecked = widget.isChecked;
|
|
||||||
super.initState();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final icon = isChecked
|
|
||||||
? svgWidget('editor/editor_check')
|
|
||||||
: svgWidget('editor/editor_uncheck');
|
|
||||||
return Align(
|
|
||||||
alignment: Alignment.centerLeft,
|
|
||||||
child: FlowyIconButton(
|
|
||||||
onPressed: () {
|
|
||||||
isChecked = !isChecked;
|
|
||||||
widget.onChanged(isChecked);
|
|
||||||
setState(() {});
|
|
||||||
},
|
|
||||||
iconPadding: EdgeInsets.zero,
|
|
||||||
icon: icon,
|
|
||||||
width: 23,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,97 +0,0 @@
|
|||||||
import 'package:flutter_quill/flutter_quill.dart';
|
|
||||||
import 'package:flutter_quill/models/documents/style.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
import 'toolbar_icon_button.dart';
|
|
||||||
|
|
||||||
class FlowyCheckListButton extends StatefulWidget {
|
|
||||||
const FlowyCheckListButton({
|
|
||||||
required this.controller,
|
|
||||||
required this.attribute,
|
|
||||||
required this.tooltipText,
|
|
||||||
this.iconSize = defaultIconSize,
|
|
||||||
this.fillColor,
|
|
||||||
this.childBuilder = defaultToggleStyleButtonBuilder,
|
|
||||||
Key? key,
|
|
||||||
}) : super(key: key);
|
|
||||||
|
|
||||||
final double iconSize;
|
|
||||||
|
|
||||||
final Color? fillColor;
|
|
||||||
|
|
||||||
final QuillController controller;
|
|
||||||
|
|
||||||
final ToggleStyleButtonBuilder childBuilder;
|
|
||||||
|
|
||||||
final Attribute attribute;
|
|
||||||
|
|
||||||
final String tooltipText;
|
|
||||||
|
|
||||||
@override
|
|
||||||
FlowyCheckListButtonState createState() => FlowyCheckListButtonState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class FlowyCheckListButtonState extends State<FlowyCheckListButton> {
|
|
||||||
bool? _isToggled;
|
|
||||||
|
|
||||||
Style get _selectionStyle => widget.controller.getSelectionStyle();
|
|
||||||
|
|
||||||
void _didChangeEditingValue() {
|
|
||||||
setState(() {
|
|
||||||
_isToggled =
|
|
||||||
_getIsToggled(widget.controller.getSelectionStyle().attributes);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
_isToggled = _getIsToggled(_selectionStyle.attributes);
|
|
||||||
widget.controller.addListener(_didChangeEditingValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool _getIsToggled(Map<String, Attribute> attrs) {
|
|
||||||
if (widget.attribute.key == Attribute.list.key) {
|
|
||||||
final attribute = attrs[widget.attribute.key];
|
|
||||||
if (attribute == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return attribute.value == widget.attribute.value ||
|
|
||||||
attribute.value == Attribute.checked.value;
|
|
||||||
}
|
|
||||||
return attrs.containsKey(widget.attribute.key);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void didUpdateWidget(covariant FlowyCheckListButton oldWidget) {
|
|
||||||
super.didUpdateWidget(oldWidget);
|
|
||||||
if (oldWidget.controller != widget.controller) {
|
|
||||||
oldWidget.controller.removeListener(_didChangeEditingValue);
|
|
||||||
widget.controller.addListener(_didChangeEditingValue);
|
|
||||||
_isToggled = _getIsToggled(_selectionStyle.attributes);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
widget.controller.removeListener(_didChangeEditingValue);
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return ToolbarIconButton(
|
|
||||||
onPressed: _toggleAttribute,
|
|
||||||
width: widget.iconSize * kIconButtonFactor,
|
|
||||||
iconName: 'editor/checkbox',
|
|
||||||
isToggled: _isToggled ?? false,
|
|
||||||
tooltipText: widget.tooltipText,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _toggleAttribute() {
|
|
||||||
widget.controller.formatSelection(_isToggled!
|
|
||||||
? Attribute.clone(Attribute.unchecked, null)
|
|
||||||
: Attribute.unchecked);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,280 +0,0 @@
|
|||||||
import 'package:flowy_infra_ui/widget/dialog/styled_dialogs.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_quill/flutter_quill.dart';
|
|
||||||
import 'package:flutter_quill/models/documents/style.dart';
|
|
||||||
import 'package:flutter_quill/utils/color.dart';
|
|
||||||
import 'package:app_flowy/generated/locale_keys.g.dart';
|
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
|
||||||
import 'toolbar_icon_button.dart';
|
|
||||||
|
|
||||||
class FlowyColorButton extends StatefulWidget {
|
|
||||||
const FlowyColorButton({
|
|
||||||
required this.icon,
|
|
||||||
required this.controller,
|
|
||||||
required this.background,
|
|
||||||
this.iconSize = defaultIconSize,
|
|
||||||
this.iconTheme,
|
|
||||||
Key? key,
|
|
||||||
}) : super(key: key);
|
|
||||||
|
|
||||||
final IconData icon;
|
|
||||||
final double iconSize;
|
|
||||||
final bool background;
|
|
||||||
final QuillController controller;
|
|
||||||
final QuillIconTheme? iconTheme;
|
|
||||||
|
|
||||||
@override
|
|
||||||
FlowyColorButtonState createState() => FlowyColorButtonState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class FlowyColorButtonState extends State<FlowyColorButton> {
|
|
||||||
late bool _isToggledColor;
|
|
||||||
late bool _isToggledBackground;
|
|
||||||
late bool _isWhite;
|
|
||||||
late bool _isWhitebackground;
|
|
||||||
|
|
||||||
Style get _selectionStyle => widget.controller.getSelectionStyle();
|
|
||||||
|
|
||||||
void _didChangeEditingValue() {
|
|
||||||
setState(() {
|
|
||||||
_isToggledColor =
|
|
||||||
_getIsToggledColor(widget.controller.getSelectionStyle().attributes);
|
|
||||||
_isToggledBackground = _getIsToggledBackground(
|
|
||||||
widget.controller.getSelectionStyle().attributes);
|
|
||||||
_isWhite = _isToggledColor &&
|
|
||||||
_selectionStyle.attributes['color']!.value == '#ffffff';
|
|
||||||
_isWhitebackground = _isToggledBackground &&
|
|
||||||
_selectionStyle.attributes['background']!.value == '#ffffff';
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
_isToggledColor = _getIsToggledColor(_selectionStyle.attributes);
|
|
||||||
_isToggledBackground = _getIsToggledBackground(_selectionStyle.attributes);
|
|
||||||
_isWhite = _isToggledColor &&
|
|
||||||
_selectionStyle.attributes['color']!.value == '#ffffff';
|
|
||||||
_isWhitebackground = _isToggledBackground &&
|
|
||||||
_selectionStyle.attributes['background']!.value == '#ffffff';
|
|
||||||
widget.controller.addListener(_didChangeEditingValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool _getIsToggledColor(Map<String, Attribute> attrs) {
|
|
||||||
return attrs.containsKey(Attribute.color.key);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool _getIsToggledBackground(Map<String, Attribute> attrs) {
|
|
||||||
return attrs.containsKey(Attribute.background.key);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void didUpdateWidget(covariant FlowyColorButton oldWidget) {
|
|
||||||
super.didUpdateWidget(oldWidget);
|
|
||||||
if (oldWidget.controller != widget.controller) {
|
|
||||||
oldWidget.controller.removeListener(_didChangeEditingValue);
|
|
||||||
widget.controller.addListener(_didChangeEditingValue);
|
|
||||||
_isToggledColor = _getIsToggledColor(_selectionStyle.attributes);
|
|
||||||
_isToggledBackground =
|
|
||||||
_getIsToggledBackground(_selectionStyle.attributes);
|
|
||||||
_isWhite = _isToggledColor &&
|
|
||||||
_selectionStyle.attributes['color']!.value == '#ffffff';
|
|
||||||
_isWhitebackground = _isToggledBackground &&
|
|
||||||
_selectionStyle.attributes['background']!.value == '#ffffff';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
widget.controller.removeListener(_didChangeEditingValue);
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final theme = Theme.of(context);
|
|
||||||
|
|
||||||
final fillColor = _isToggledColor && !widget.background && _isWhite
|
|
||||||
? stringToColor('#ffffff')
|
|
||||||
: (widget.iconTheme?.iconUnselectedFillColor ?? theme.canvasColor);
|
|
||||||
final fillColorBackground =
|
|
||||||
_isToggledBackground && widget.background && _isWhitebackground
|
|
||||||
? stringToColor('#ffffff')
|
|
||||||
: (widget.iconTheme?.iconUnselectedFillColor ?? theme.canvasColor);
|
|
||||||
|
|
||||||
return Tooltip(
|
|
||||||
message: LocaleKeys.toolbar_highlight.tr(),
|
|
||||||
showDuration: Duration.zero,
|
|
||||||
child: QuillIconButton(
|
|
||||||
highlightElevation: 0,
|
|
||||||
hoverElevation: 0,
|
|
||||||
size: widget.iconSize * kIconButtonFactor,
|
|
||||||
icon: Icon(widget.icon,
|
|
||||||
size: widget.iconSize, color: theme.iconTheme.color),
|
|
||||||
fillColor: widget.background ? fillColorBackground : fillColor,
|
|
||||||
onPressed: _showColorPicker,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _changeColor(BuildContext context, Color color) {
|
|
||||||
var hex = color.value.toRadixString(16);
|
|
||||||
if (hex.startsWith('ff')) {
|
|
||||||
hex = hex.substring(2);
|
|
||||||
}
|
|
||||||
hex = '#$hex';
|
|
||||||
widget.controller.formatSelection(
|
|
||||||
widget.background ? BackgroundAttribute(hex) : ColorAttribute(hex));
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
}
|
|
||||||
|
|
||||||
void _showColorPicker() {
|
|
||||||
final style = widget.controller.getSelectionStyle();
|
|
||||||
final values = style.values
|
|
||||||
.where((v) => v.key == Attribute.background.key)
|
|
||||||
.map((v) => v.value);
|
|
||||||
int initialColor = 0;
|
|
||||||
if (values.isNotEmpty) {
|
|
||||||
assert(values.length == 1);
|
|
||||||
initialColor = stringToHex(values.first);
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledDialog(
|
|
||||||
child: SingleChildScrollView(
|
|
||||||
child: FlowyColorPicker(
|
|
||||||
onColorChanged: (color) {
|
|
||||||
if (color == null) {
|
|
||||||
widget.controller.formatSelection(BackgroundAttribute(null));
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
} else {
|
|
||||||
_changeColor(context, color);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
initialColor: initialColor,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
).show(context);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int stringToHex(String code) {
|
|
||||||
return int.parse(code.substring(1, 7), radix: 16) + 0xFF000000;
|
|
||||||
}
|
|
||||||
|
|
||||||
class FlowyColorPicker extends StatefulWidget {
|
|
||||||
final List<int> colors = [
|
|
||||||
0xffe8e0ff,
|
|
||||||
0xffffe7fd,
|
|
||||||
0xffffe7ee,
|
|
||||||
0xffffefe3,
|
|
||||||
0xfffff2cd,
|
|
||||||
0xfff5ffdc,
|
|
||||||
0xffddffd6,
|
|
||||||
0xffdefff1,
|
|
||||||
];
|
|
||||||
final Function(Color?) onColorChanged;
|
|
||||||
final int initialColor;
|
|
||||||
FlowyColorPicker(
|
|
||||||
{Key? key, required this.onColorChanged, this.initialColor = 0})
|
|
||||||
: super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<FlowyColorPicker> createState() => _FlowyColorPickerState();
|
|
||||||
}
|
|
||||||
|
|
||||||
// if (shrinkWrap) {
|
|
||||||
// innerContent = IntrinsicWidth(child: IntrinsicHeight(child: innerContent));
|
|
||||||
// }
|
|
||||||
class _FlowyColorPickerState extends State<FlowyColorPicker> {
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
const double width = 480;
|
|
||||||
const int crossAxisCount = 6;
|
|
||||||
const double mainAxisSpacing = 10;
|
|
||||||
const double crossAxisSpacing = 10;
|
|
||||||
final numberOfRows = (widget.colors.length / crossAxisCount).ceil();
|
|
||||||
|
|
||||||
const perRowHeight =
|
|
||||||
((width - ((crossAxisCount - 1) * mainAxisSpacing)) / crossAxisCount);
|
|
||||||
final totalHeight =
|
|
||||||
numberOfRows * perRowHeight + numberOfRows * crossAxisSpacing;
|
|
||||||
|
|
||||||
return Container(
|
|
||||||
constraints: BoxConstraints.tightFor(width: width, height: totalHeight),
|
|
||||||
child: CustomScrollView(
|
|
||||||
scrollDirection: Axis.vertical,
|
|
||||||
controller: ScrollController(),
|
|
||||||
physics: const ClampingScrollPhysics(),
|
|
||||||
slivers: [
|
|
||||||
SliverGrid(
|
|
||||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
|
||||||
crossAxisCount: crossAxisCount,
|
|
||||||
mainAxisSpacing: mainAxisSpacing,
|
|
||||||
crossAxisSpacing: crossAxisSpacing,
|
|
||||||
childAspectRatio: 1.0,
|
|
||||||
),
|
|
||||||
delegate: SliverChildBuilderDelegate(
|
|
||||||
(BuildContext context, int index) {
|
|
||||||
if (widget.colors.length > index) {
|
|
||||||
final isSelected =
|
|
||||||
widget.colors[index] == widget.initialColor;
|
|
||||||
return ColorItem(
|
|
||||||
color: Color(widget.colors[index]),
|
|
||||||
onPressed: widget.onColorChanged,
|
|
||||||
isSelected: isSelected,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
childCount: widget.colors.length,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class ColorItem extends StatelessWidget {
|
|
||||||
final Function(Color?) onPressed;
|
|
||||||
final bool isSelected;
|
|
||||||
final Color color;
|
|
||||||
const ColorItem({
|
|
||||||
Key? key,
|
|
||||||
required this.color,
|
|
||||||
required this.onPressed,
|
|
||||||
this.isSelected = false,
|
|
||||||
}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
if (!isSelected) {
|
|
||||||
return RawMaterialButton(
|
|
||||||
onPressed: () {
|
|
||||||
onPressed(color);
|
|
||||||
},
|
|
||||||
elevation: 0,
|
|
||||||
hoverElevation: 0.6,
|
|
||||||
fillColor: color,
|
|
||||||
shape: const CircleBorder(),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return RawMaterialButton(
|
|
||||||
shape: const CircleBorder(
|
|
||||||
side: BorderSide(color: Colors.white, width: 8)) +
|
|
||||||
CircleBorder(side: BorderSide(color: color, width: 4)),
|
|
||||||
onPressed: () {
|
|
||||||
if (isSelected) {
|
|
||||||
onPressed(null);
|
|
||||||
} else {
|
|
||||||
onPressed(color);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
elevation: 1.0,
|
|
||||||
hoverElevation: 0.6,
|
|
||||||
fillColor: color,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,101 +0,0 @@
|
|||||||
import 'package:flutter_quill/flutter_quill.dart';
|
|
||||||
import 'package:flutter_quill/models/documents/style.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:app_flowy/generated/locale_keys.g.dart';
|
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
|
||||||
import 'toolbar_icon_button.dart';
|
|
||||||
|
|
||||||
class FlowyHeaderStyleButton extends StatefulWidget {
|
|
||||||
const FlowyHeaderStyleButton({
|
|
||||||
required this.controller,
|
|
||||||
this.iconSize = defaultIconSize,
|
|
||||||
Key? key,
|
|
||||||
}) : super(key: key);
|
|
||||||
|
|
||||||
final QuillController controller;
|
|
||||||
final double iconSize;
|
|
||||||
|
|
||||||
@override
|
|
||||||
FlowyHeaderStyleButtonState createState() => FlowyHeaderStyleButtonState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class FlowyHeaderStyleButtonState extends State<FlowyHeaderStyleButton> {
|
|
||||||
Attribute? _value;
|
|
||||||
|
|
||||||
Style get _selectionStyle => widget.controller.getSelectionStyle();
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
setState(() {
|
|
||||||
_value =
|
|
||||||
_selectionStyle.attributes[Attribute.header.key] ?? Attribute.header;
|
|
||||||
});
|
|
||||||
widget.controller.addListener(_didChangeEditingValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final valueToText = <Attribute, String>{
|
|
||||||
Attribute.h1: 'H1',
|
|
||||||
Attribute.h2: 'H2',
|
|
||||||
Attribute.h3: 'H3',
|
|
||||||
};
|
|
||||||
|
|
||||||
final valueAttribute = <Attribute>[
|
|
||||||
Attribute.h1,
|
|
||||||
Attribute.h2,
|
|
||||||
Attribute.h3
|
|
||||||
];
|
|
||||||
final valueString = <String>['H1', 'H2', 'H3'];
|
|
||||||
final attributeImageName = <String>['editor/H1', 'editor/H2', 'editor/H3'];
|
|
||||||
|
|
||||||
return Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: List.generate(3, (index) {
|
|
||||||
// final child =
|
|
||||||
// _valueToText[_value] == _valueString[index] ? svg('editor/H1', color: Colors.white) : svg('editor/H1');
|
|
||||||
|
|
||||||
final headerTitle = "${LocaleKeys.toolbar_header.tr()} ${index + 1}";
|
|
||||||
final isToggled = valueToText[_value] == valueString[index];
|
|
||||||
return ToolbarIconButton(
|
|
||||||
onPressed: () {
|
|
||||||
if (isToggled) {
|
|
||||||
widget.controller.formatSelection(Attribute.header);
|
|
||||||
} else {
|
|
||||||
widget.controller.formatSelection(valueAttribute[index]);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
width: widget.iconSize * kIconButtonFactor,
|
|
||||||
iconName: attributeImageName[index],
|
|
||||||
isToggled: isToggled,
|
|
||||||
tooltipText: headerTitle,
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _didChangeEditingValue() {
|
|
||||||
setState(() {
|
|
||||||
_value =
|
|
||||||
_selectionStyle.attributes[Attribute.header.key] ?? Attribute.header;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void didUpdateWidget(covariant FlowyHeaderStyleButton oldWidget) {
|
|
||||||
super.didUpdateWidget(oldWidget);
|
|
||||||
if (oldWidget.controller != widget.controller) {
|
|
||||||
oldWidget.controller.removeListener(_didChangeEditingValue);
|
|
||||||
widget.controller.addListener(_didChangeEditingValue);
|
|
||||||
_value =
|
|
||||||
_selectionStyle.attributes[Attribute.header.key] ?? Attribute.header;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
widget.controller.removeListener(_didChangeEditingValue);
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,33 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_quill/flutter_quill.dart';
|
|
||||||
|
|
||||||
class FlowyHistoryButton extends StatelessWidget {
|
|
||||||
final IconData icon;
|
|
||||||
final double iconSize;
|
|
||||||
final bool undo;
|
|
||||||
final QuillController controller;
|
|
||||||
final String tooltipText;
|
|
||||||
|
|
||||||
const FlowyHistoryButton({
|
|
||||||
required this.icon,
|
|
||||||
required this.controller,
|
|
||||||
required this.undo,
|
|
||||||
required this.tooltipText,
|
|
||||||
required this.iconSize,
|
|
||||||
Key? key,
|
|
||||||
}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Tooltip(
|
|
||||||
message: tooltipText,
|
|
||||||
showDuration: Duration.zero,
|
|
||||||
child: HistoryButton(
|
|
||||||
icon: icon,
|
|
||||||
iconSize: iconSize,
|
|
||||||
controller: controller,
|
|
||||||
undo: undo,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,85 +0,0 @@
|
|||||||
import 'package:flutter_quill/flutter_quill.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'toolbar_icon_button.dart';
|
|
||||||
|
|
||||||
class FlowyImageButton extends StatelessWidget {
|
|
||||||
const FlowyImageButton({
|
|
||||||
required this.controller,
|
|
||||||
required this.tooltipText,
|
|
||||||
this.iconSize = defaultIconSize,
|
|
||||||
this.onImagePickCallback,
|
|
||||||
this.fillColor,
|
|
||||||
this.filePickImpl,
|
|
||||||
this.webImagePickImpl,
|
|
||||||
this.mediaPickSettingSelector,
|
|
||||||
Key? key,
|
|
||||||
}) : super(key: key);
|
|
||||||
|
|
||||||
final double iconSize;
|
|
||||||
|
|
||||||
final Color? fillColor;
|
|
||||||
|
|
||||||
final QuillController controller;
|
|
||||||
|
|
||||||
final OnImagePickCallback? onImagePickCallback;
|
|
||||||
|
|
||||||
final WebImagePickImpl? webImagePickImpl;
|
|
||||||
|
|
||||||
final FilePickImpl? filePickImpl;
|
|
||||||
|
|
||||||
final MediaPickSettingSelector? mediaPickSettingSelector;
|
|
||||||
|
|
||||||
final String tooltipText;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return ToolbarIconButton(
|
|
||||||
iconName: 'editor/image',
|
|
||||||
width: iconSize * 1.77,
|
|
||||||
onPressed: () => _onPressedHandler(context),
|
|
||||||
isToggled: false,
|
|
||||||
tooltipText: tooltipText,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _onPressedHandler(BuildContext context) async {
|
|
||||||
// if (onImagePickCallback != null) {
|
|
||||||
// final selector = mediaPickSettingSelector ?? ImageVideoUtils.selectMediaPickSetting;
|
|
||||||
// final source = await selector(context);
|
|
||||||
// if (source != null) {
|
|
||||||
// if (source == MediaPickSetting.Gallery) {
|
|
||||||
// _pickImage(context);
|
|
||||||
// } else {
|
|
||||||
// _typeLink(context);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// } else {
|
|
||||||
// _typeLink(context);
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
|
|
||||||
// void _pickImage(BuildContext context) => ImageVideoUtils.handleImageButtonTap(
|
|
||||||
// context,
|
|
||||||
// controller,
|
|
||||||
// ImageSource.gallery,
|
|
||||||
// onImagePickCallback!,
|
|
||||||
// filePickImpl: filePickImpl,
|
|
||||||
// webImagePickImpl: webImagePickImpl,
|
|
||||||
// );
|
|
||||||
|
|
||||||
// void _typeLink(BuildContext context) {
|
|
||||||
// TextFieldDialog(
|
|
||||||
// title: 'URL',
|
|
||||||
// value: "",
|
|
||||||
// confirm: (newValue) {
|
|
||||||
// if (newValue.isEmpty) {
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
// final index = controller.selection.baseOffset;
|
|
||||||
// final length = controller.selection.extentOffset - index;
|
|
||||||
|
|
||||||
// controller.replaceText(index, length, BlockEmbed.image(newValue), null);
|
|
||||||
// },
|
|
||||||
// ).show(context);
|
|
||||||
// }
|
|
||||||
}
|
|
@ -1,98 +0,0 @@
|
|||||||
import 'package:app_flowy/workspace/presentation/widgets/dialogs.dart';
|
|
||||||
import 'package:flutter_quill/flutter_quill.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/material.dart';
|
|
||||||
import 'package:provider/provider.dart';
|
|
||||||
|
|
||||||
import 'toolbar_icon_button.dart';
|
|
||||||
|
|
||||||
class FlowyLinkStyleButton extends StatefulWidget {
|
|
||||||
const FlowyLinkStyleButton({
|
|
||||||
required this.controller,
|
|
||||||
this.iconSize = defaultIconSize,
|
|
||||||
Key? key,
|
|
||||||
}) : super(key: key);
|
|
||||||
|
|
||||||
final QuillController controller;
|
|
||||||
final double iconSize;
|
|
||||||
|
|
||||||
@override
|
|
||||||
FlowyLinkStyleButtonState createState() => FlowyLinkStyleButtonState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class FlowyLinkStyleButtonState extends State<FlowyLinkStyleButton> {
|
|
||||||
void _didChangeSelection() {
|
|
||||||
setState(() {});
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
widget.controller.addListener(_didChangeSelection);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void didUpdateWidget(covariant FlowyLinkStyleButton oldWidget) {
|
|
||||||
super.didUpdateWidget(oldWidget);
|
|
||||||
if (oldWidget.controller != widget.controller) {
|
|
||||||
oldWidget.controller.removeListener(_didChangeSelection);
|
|
||||||
widget.controller.addListener(_didChangeSelection);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
super.dispose();
|
|
||||||
widget.controller.removeListener(_didChangeSelection);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final theme = context.watch<AppTheme>();
|
|
||||||
final isEnabled = !widget.controller.selection.isCollapsed;
|
|
||||||
final pressedHandler = isEnabled ? () => _openLinkDialog(context) : null;
|
|
||||||
final icon = isEnabled
|
|
||||||
? svgWidget(
|
|
||||||
'editor/share',
|
|
||||||
color: theme.iconColor,
|
|
||||||
)
|
|
||||||
: svgWidget(
|
|
||||||
'editor/share',
|
|
||||||
color: theme.disableIconColor,
|
|
||||||
);
|
|
||||||
|
|
||||||
return FlowyIconButton(
|
|
||||||
onPressed: pressedHandler,
|
|
||||||
iconPadding: const EdgeInsets.symmetric(horizontal: 4, vertical: 4),
|
|
||||||
icon: icon,
|
|
||||||
fillColor: theme.shader6,
|
|
||||||
hoverColor: theme.shader5,
|
|
||||||
width: widget.iconSize * kIconButtonFactor,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _openLinkDialog(BuildContext context) {
|
|
||||||
final style = widget.controller.getSelectionStyle();
|
|
||||||
final values = style.values
|
|
||||||
.where((v) => v.key == Attribute.link.key)
|
|
||||||
.map((v) => v.value);
|
|
||||||
String value = "";
|
|
||||||
if (values.isNotEmpty) {
|
|
||||||
assert(values.length == 1);
|
|
||||||
value = values.first;
|
|
||||||
}
|
|
||||||
|
|
||||||
NavigatorTextFieldDialog(
|
|
||||||
title: 'URL',
|
|
||||||
value: value,
|
|
||||||
confirm: (newValue) {
|
|
||||||
if (newValue.isEmpty) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
widget.controller.formatSelection(LinkAttribute(newValue));
|
|
||||||
},
|
|
||||||
).show(context);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,84 +0,0 @@
|
|||||||
import 'package:flutter_quill/flutter_quill.dart';
|
|
||||||
import 'package:flutter_quill/models/documents/style.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
import 'toolbar_icon_button.dart';
|
|
||||||
|
|
||||||
class FlowyToggleStyleButton extends StatefulWidget {
|
|
||||||
final Attribute attribute;
|
|
||||||
final String normalIcon;
|
|
||||||
final double iconSize;
|
|
||||||
final QuillController controller;
|
|
||||||
final String tooltipText;
|
|
||||||
|
|
||||||
const FlowyToggleStyleButton({
|
|
||||||
required this.attribute,
|
|
||||||
required this.normalIcon,
|
|
||||||
required this.controller,
|
|
||||||
required this.tooltipText,
|
|
||||||
this.iconSize = defaultIconSize,
|
|
||||||
Key? key,
|
|
||||||
}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
ToggleStyleButtonState createState() => ToggleStyleButtonState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class ToggleStyleButtonState extends State<FlowyToggleStyleButton> {
|
|
||||||
bool? _isToggled;
|
|
||||||
Style get _selectionStyle => widget.controller.getSelectionStyle();
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
_isToggled = _getIsToggled(_selectionStyle.attributes);
|
|
||||||
widget.controller.addListener(_didChangeEditingValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return ToolbarIconButton(
|
|
||||||
onPressed: _toggleAttribute,
|
|
||||||
width: widget.iconSize * kIconButtonFactor,
|
|
||||||
isToggled: _isToggled ?? false,
|
|
||||||
iconName: widget.normalIcon,
|
|
||||||
tooltipText: widget.tooltipText,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void didUpdateWidget(covariant FlowyToggleStyleButton oldWidget) {
|
|
||||||
super.didUpdateWidget(oldWidget);
|
|
||||||
if (oldWidget.controller != widget.controller) {
|
|
||||||
oldWidget.controller.removeListener(_didChangeEditingValue);
|
|
||||||
widget.controller.addListener(_didChangeEditingValue);
|
|
||||||
_isToggled = _getIsToggled(_selectionStyle.attributes);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
widget.controller.removeListener(_didChangeEditingValue);
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
void _didChangeEditingValue() {
|
|
||||||
setState(() => _isToggled = _getIsToggled(_selectionStyle.attributes));
|
|
||||||
}
|
|
||||||
|
|
||||||
bool _getIsToggled(Map<String, Attribute> attrs) {
|
|
||||||
if (widget.attribute.key == Attribute.list.key) {
|
|
||||||
final attribute = attrs[widget.attribute.key];
|
|
||||||
if (attribute == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return attribute.value == widget.attribute.value;
|
|
||||||
}
|
|
||||||
return attrs.containsKey(widget.attribute.key);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _toggleAttribute() {
|
|
||||||
widget.controller.formatSelection(_isToggled!
|
|
||||||
? Attribute.clone(widget.attribute, null)
|
|
||||||
: widget.attribute);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,308 +0,0 @@
|
|||||||
import 'dart:async';
|
|
||||||
import 'dart:math';
|
|
||||||
|
|
||||||
import 'package:app_flowy/workspace/presentation/widgets/emoji_picker/emoji_picker.dart';
|
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
|
||||||
import 'package:flutter_quill/flutter_quill.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:styled_widget/styled_widget.dart';
|
|
||||||
import 'check_button.dart';
|
|
||||||
import 'color_picker.dart';
|
|
||||||
import 'header_button.dart';
|
|
||||||
import 'history_button.dart';
|
|
||||||
import 'link_button.dart';
|
|
||||||
import 'toggle_button.dart';
|
|
||||||
import 'toolbar_icon_button.dart';
|
|
||||||
import 'package:app_flowy/generated/locale_keys.g.dart';
|
|
||||||
|
|
||||||
class EditorToolbar extends StatelessWidget implements PreferredSizeWidget {
|
|
||||||
final List<Widget> children;
|
|
||||||
final double toolBarHeight;
|
|
||||||
final Color? color;
|
|
||||||
|
|
||||||
const EditorToolbar({
|
|
||||||
required this.children,
|
|
||||||
this.toolBarHeight = 46,
|
|
||||||
this.color,
|
|
||||||
Key? key,
|
|
||||||
}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Container(
|
|
||||||
color: Theme.of(context).canvasColor,
|
|
||||||
constraints: BoxConstraints.tightFor(height: preferredSize.height),
|
|
||||||
child: ToolbarButtonList(buttons: children)
|
|
||||||
.padding(horizontal: 4, vertical: 4),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Size get preferredSize => Size.fromHeight(toolBarHeight);
|
|
||||||
|
|
||||||
factory EditorToolbar.basic({
|
|
||||||
required QuillController controller,
|
|
||||||
double toolbarIconSize = defaultIconSize,
|
|
||||||
OnImagePickCallback? onImagePickCallback,
|
|
||||||
OnVideoPickCallback? onVideoPickCallback,
|
|
||||||
MediaPickSettingSelector? mediaPickSettingSelector,
|
|
||||||
FilePickImpl? filePickImpl,
|
|
||||||
WebImagePickImpl? webImagePickImpl,
|
|
||||||
WebVideoPickImpl? webVideoPickImpl,
|
|
||||||
Key? key,
|
|
||||||
}) {
|
|
||||||
return EditorToolbar(
|
|
||||||
key: key,
|
|
||||||
toolBarHeight: toolbarIconSize * 2,
|
|
||||||
children: [
|
|
||||||
FlowyHistoryButton(
|
|
||||||
icon: Icons.undo_outlined,
|
|
||||||
iconSize: toolbarIconSize,
|
|
||||||
controller: controller,
|
|
||||||
undo: true,
|
|
||||||
tooltipText: LocaleKeys.toolbar_undo.tr(),
|
|
||||||
),
|
|
||||||
FlowyHistoryButton(
|
|
||||||
icon: Icons.redo_outlined,
|
|
||||||
iconSize: toolbarIconSize,
|
|
||||||
controller: controller,
|
|
||||||
undo: false,
|
|
||||||
tooltipText: LocaleKeys.toolbar_redo.tr(),
|
|
||||||
),
|
|
||||||
FlowyToggleStyleButton(
|
|
||||||
attribute: Attribute.bold,
|
|
||||||
normalIcon: 'editor/bold',
|
|
||||||
iconSize: toolbarIconSize,
|
|
||||||
controller: controller,
|
|
||||||
tooltipText: LocaleKeys.toolbar_bold.tr(),
|
|
||||||
),
|
|
||||||
FlowyToggleStyleButton(
|
|
||||||
attribute: Attribute.italic,
|
|
||||||
normalIcon: 'editor/italic',
|
|
||||||
iconSize: toolbarIconSize,
|
|
||||||
controller: controller,
|
|
||||||
tooltipText: LocaleKeys.toolbar_italic.tr(),
|
|
||||||
),
|
|
||||||
FlowyToggleStyleButton(
|
|
||||||
attribute: Attribute.underline,
|
|
||||||
normalIcon: 'editor/underline',
|
|
||||||
iconSize: toolbarIconSize,
|
|
||||||
controller: controller,
|
|
||||||
tooltipText: LocaleKeys.toolbar_underline.tr(),
|
|
||||||
),
|
|
||||||
FlowyToggleStyleButton(
|
|
||||||
attribute: Attribute.strikeThrough,
|
|
||||||
normalIcon: 'editor/strikethrough',
|
|
||||||
iconSize: toolbarIconSize,
|
|
||||||
controller: controller,
|
|
||||||
tooltipText: LocaleKeys.toolbar_strike.tr(),
|
|
||||||
),
|
|
||||||
FlowyColorButton(
|
|
||||||
icon: Icons.format_color_fill,
|
|
||||||
iconSize: toolbarIconSize,
|
|
||||||
controller: controller,
|
|
||||||
background: true,
|
|
||||||
),
|
|
||||||
// FlowyImageButton(
|
|
||||||
// iconSize: toolbarIconSize,
|
|
||||||
// controller: controller,
|
|
||||||
// onImagePickCallback: onImagePickCallback,
|
|
||||||
// filePickImpl: filePickImpl,
|
|
||||||
// webImagePickImpl: webImagePickImpl,
|
|
||||||
// mediaPickSettingSelector: mediaPickSettingSelector,
|
|
||||||
// ),
|
|
||||||
FlowyHeaderStyleButton(
|
|
||||||
controller: controller,
|
|
||||||
iconSize: toolbarIconSize,
|
|
||||||
),
|
|
||||||
FlowyToggleStyleButton(
|
|
||||||
attribute: Attribute.ol,
|
|
||||||
controller: controller,
|
|
||||||
normalIcon: 'editor/numbers',
|
|
||||||
iconSize: toolbarIconSize,
|
|
||||||
tooltipText: LocaleKeys.toolbar_numList.tr(),
|
|
||||||
),
|
|
||||||
FlowyToggleStyleButton(
|
|
||||||
attribute: Attribute.ul,
|
|
||||||
controller: controller,
|
|
||||||
normalIcon: 'editor/bullet_list',
|
|
||||||
iconSize: toolbarIconSize,
|
|
||||||
tooltipText: LocaleKeys.toolbar_bulletList.tr(),
|
|
||||||
),
|
|
||||||
FlowyCheckListButton(
|
|
||||||
attribute: Attribute.unchecked,
|
|
||||||
controller: controller,
|
|
||||||
iconSize: toolbarIconSize,
|
|
||||||
tooltipText: LocaleKeys.toolbar_checkList.tr(),
|
|
||||||
),
|
|
||||||
FlowyToggleStyleButton(
|
|
||||||
attribute: Attribute.inlineCode,
|
|
||||||
controller: controller,
|
|
||||||
normalIcon: 'editor/inline_block',
|
|
||||||
iconSize: toolbarIconSize,
|
|
||||||
tooltipText: LocaleKeys.toolbar_inlineCode.tr(),
|
|
||||||
),
|
|
||||||
FlowyToggleStyleButton(
|
|
||||||
attribute: Attribute.blockQuote,
|
|
||||||
controller: controller,
|
|
||||||
normalIcon: 'editor/quote',
|
|
||||||
iconSize: toolbarIconSize,
|
|
||||||
tooltipText: LocaleKeys.toolbar_quote.tr(),
|
|
||||||
),
|
|
||||||
FlowyLinkStyleButton(
|
|
||||||
controller: controller,
|
|
||||||
iconSize: toolbarIconSize,
|
|
||||||
),
|
|
||||||
FlowyEmojiStyleButton(
|
|
||||||
normalIcon: 'editor/insert_emoticon',
|
|
||||||
controller: controller,
|
|
||||||
tooltipText: "Emoji Picker",
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class ToolbarButtonList extends StatefulWidget {
|
|
||||||
const ToolbarButtonList({required this.buttons, Key? key}) : super(key: key);
|
|
||||||
|
|
||||||
final List<Widget> buttons;
|
|
||||||
|
|
||||||
@override
|
|
||||||
ToolbarButtonListState createState() => ToolbarButtonListState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class ToolbarButtonListState extends State<ToolbarButtonList>
|
|
||||||
with WidgetsBindingObserver {
|
|
||||||
final ScrollController _controller = ScrollController();
|
|
||||||
bool _showLeftArrow = false;
|
|
||||||
bool _showRightArrow = false;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
_controller.addListener(_handleScroll);
|
|
||||||
|
|
||||||
// Listening to the WidgetsBinding instance is necessary so that we can
|
|
||||||
// hide the arrows when the window gets a new size and thus the toolbar
|
|
||||||
// becomes scrollable/unscrollable.
|
|
||||||
WidgetsBinding.instance.addObserver(this);
|
|
||||||
|
|
||||||
// Workaround to allow the scroll controller attach to our ListView so that
|
|
||||||
// we can detect if overflow arrows need to be shown on init.
|
|
||||||
Timer.run(_handleScroll);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return LayoutBuilder(
|
|
||||||
builder: (BuildContext context, BoxConstraints constraints) {
|
|
||||||
List<Widget> children = [];
|
|
||||||
double width =
|
|
||||||
(widget.buttons.length + 2) * defaultIconSize * kIconButtonFactor;
|
|
||||||
final isFit = constraints.maxWidth > width;
|
|
||||||
if (!isFit) {
|
|
||||||
children.add(_buildLeftArrow());
|
|
||||||
width = width + 18;
|
|
||||||
}
|
|
||||||
|
|
||||||
children.add(_buildScrollableList(constraints, isFit));
|
|
||||||
|
|
||||||
if (!isFit) {
|
|
||||||
children.add(_buildRightArrow());
|
|
||||||
width = width + 18;
|
|
||||||
}
|
|
||||||
|
|
||||||
return SizedBox(
|
|
||||||
width: min(constraints.maxWidth, width),
|
|
||||||
child: Row(
|
|
||||||
children: children,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void didChangeMetrics() => _handleScroll();
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
_controller.dispose();
|
|
||||||
WidgetsBinding.instance.removeObserver(this);
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
void _handleScroll() {
|
|
||||||
if (!mounted) return;
|
|
||||||
setState(() {
|
|
||||||
_showLeftArrow =
|
|
||||||
_controller.position.minScrollExtent != _controller.position.pixels;
|
|
||||||
_showRightArrow =
|
|
||||||
_controller.position.maxScrollExtent != _controller.position.pixels;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildLeftArrow() {
|
|
||||||
return SizedBox(
|
|
||||||
width: 8,
|
|
||||||
child: Transform.translate(
|
|
||||||
// Move the icon a few pixels to center it
|
|
||||||
offset: const Offset(-5, 0),
|
|
||||||
child: _showLeftArrow ? const Icon(Icons.arrow_left, size: 18) : null,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// [[sliver: https://medium.com/flutter/slivers-demystified-6ff68ab0296f]]
|
|
||||||
Widget _buildScrollableList(BoxConstraints constraints, bool isFit) {
|
|
||||||
Widget child = Expanded(
|
|
||||||
child: CustomScrollView(
|
|
||||||
scrollDirection: Axis.horizontal,
|
|
||||||
controller: _controller,
|
|
||||||
physics: const ClampingScrollPhysics(),
|
|
||||||
slivers: [
|
|
||||||
SliverList(
|
|
||||||
delegate: SliverChildBuilderDelegate(
|
|
||||||
(BuildContext context, int index) {
|
|
||||||
return widget.buttons[index];
|
|
||||||
},
|
|
||||||
childCount: widget.buttons.length,
|
|
||||||
addAutomaticKeepAlives: false,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!isFit) {
|
|
||||||
child = ScrollConfiguration(
|
|
||||||
// Remove the glowing effect, as we already have the arrow indicators
|
|
||||||
behavior: _NoGlowBehavior(),
|
|
||||||
// The CustomScrollView is necessary so that the children are not
|
|
||||||
// stretched to the height of the toolbar, https://bit.ly/3uC3bjI
|
|
||||||
child: child,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return child;
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildRightArrow() {
|
|
||||||
return SizedBox(
|
|
||||||
width: 8,
|
|
||||||
child: Transform.translate(
|
|
||||||
// Move the icon a few pixels to center it
|
|
||||||
offset: const Offset(-5, 0),
|
|
||||||
child: _showRightArrow ? const Icon(Icons.arrow_right, size: 18) : null,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _NoGlowBehavior extends ScrollBehavior {
|
|
||||||
@override
|
|
||||||
Widget buildViewportChrome(BuildContext _, Widget child, AxisDirection __) {
|
|
||||||
return child;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,38 +0,0 @@
|
|||||||
import 'package:flowy_infra/image.dart';
|
|
||||||
import 'package:flowy_infra_ui/style_widget/icon_button.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flowy_infra/theme.dart';
|
|
||||||
import 'package:provider/provider.dart';
|
|
||||||
|
|
||||||
const double defaultIconSize = 18;
|
|
||||||
|
|
||||||
class ToolbarIconButton extends StatelessWidget {
|
|
||||||
final double width;
|
|
||||||
final VoidCallback? onPressed;
|
|
||||||
final bool isToggled;
|
|
||||||
final String iconName;
|
|
||||||
final String tooltipText;
|
|
||||||
|
|
||||||
const ToolbarIconButton({
|
|
||||||
Key? key,
|
|
||||||
required this.onPressed,
|
|
||||||
required this.isToggled,
|
|
||||||
required this.width,
|
|
||||||
required this.iconName,
|
|
||||||
required this.tooltipText,
|
|
||||||
}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final theme = context.watch<AppTheme>();
|
|
||||||
return FlowyIconButton(
|
|
||||||
iconPadding: const EdgeInsets.symmetric(horizontal: 4, vertical: 4),
|
|
||||||
onPressed: onPressed,
|
|
||||||
width: width,
|
|
||||||
icon: isToggled == true ? svgWidget(iconName, color: Colors.white) : svgWidget(iconName, color: theme.iconColor),
|
|
||||||
fillColor: isToggled == true ? theme.main1 : theme.shader6,
|
|
||||||
hoverColor: isToggled == true ? theme.main1 : theme.hover,
|
|
||||||
tooltipText: tooltipText,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,133 +0,0 @@
|
|||||||
import 'package:app_flowy/plugins/doc/presentation/style_widgets.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_quill/flutter_quill.dart';
|
|
||||||
import 'package:provider/provider.dart';
|
|
||||||
import 'package:tuple/tuple.dart';
|
|
||||||
import 'package:flowy_infra/theme.dart';
|
|
||||||
|
|
||||||
DefaultStyles customStyles(BuildContext context) {
|
|
||||||
const baseSpacing = Tuple2<double, double>(6, 0);
|
|
||||||
|
|
||||||
final theme = context.watch<AppTheme>();
|
|
||||||
final themeData = theme.themeData;
|
|
||||||
final fontFamily = makeFontFamily(themeData);
|
|
||||||
|
|
||||||
final defaultTextStyle = DefaultTextStyle.of(context);
|
|
||||||
final baseStyle = defaultTextStyle.style.copyWith(
|
|
||||||
fontSize: 18,
|
|
||||||
height: 1.3,
|
|
||||||
fontWeight: FontWeight.w300,
|
|
||||||
letterSpacing: 0.6,
|
|
||||||
fontFamily: fontFamily,
|
|
||||||
);
|
|
||||||
|
|
||||||
return DefaultStyles(
|
|
||||||
h1: DefaultTextBlockStyle(
|
|
||||||
defaultTextStyle.style.copyWith(
|
|
||||||
fontSize: 34,
|
|
||||||
color: defaultTextStyle.style.color!.withOpacity(0.70),
|
|
||||||
height: 1.15,
|
|
||||||
fontWeight: FontWeight.w300,
|
|
||||||
),
|
|
||||||
const Tuple2(16, 0),
|
|
||||||
const Tuple2(0, 0),
|
|
||||||
null),
|
|
||||||
h2: DefaultTextBlockStyle(
|
|
||||||
defaultTextStyle.style.copyWith(
|
|
||||||
fontSize: 24,
|
|
||||||
color: defaultTextStyle.style.color!.withOpacity(0.70),
|
|
||||||
height: 1.15,
|
|
||||||
fontWeight: FontWeight.normal,
|
|
||||||
),
|
|
||||||
const Tuple2(8, 0),
|
|
||||||
const Tuple2(0, 0),
|
|
||||||
null),
|
|
||||||
h3: DefaultTextBlockStyle(
|
|
||||||
defaultTextStyle.style.copyWith(
|
|
||||||
fontSize: 20,
|
|
||||||
color: defaultTextStyle.style.color!.withOpacity(0.70),
|
|
||||||
height: 1.25,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
),
|
|
||||||
const Tuple2(8, 0),
|
|
||||||
const Tuple2(0, 0),
|
|
||||||
null),
|
|
||||||
paragraph: DefaultTextBlockStyle(
|
|
||||||
baseStyle, const Tuple2(10, 0), const Tuple2(0, 0), null),
|
|
||||||
bold: const TextStyle(fontWeight: FontWeight.bold),
|
|
||||||
italic: const TextStyle(fontStyle: FontStyle.italic),
|
|
||||||
small: const TextStyle(fontSize: 12, color: Colors.black45),
|
|
||||||
underline: const TextStyle(decoration: TextDecoration.underline),
|
|
||||||
strikeThrough: const TextStyle(decoration: TextDecoration.lineThrough),
|
|
||||||
inlineCode: TextStyle(
|
|
||||||
color: Colors.blue.shade900.withOpacity(0.9),
|
|
||||||
fontFamily: fontFamily,
|
|
||||||
fontSize: 13,
|
|
||||||
),
|
|
||||||
link: TextStyle(
|
|
||||||
color: themeData.colorScheme.secondary,
|
|
||||||
decoration: TextDecoration.underline,
|
|
||||||
),
|
|
||||||
color: theme.textColor,
|
|
||||||
placeHolder: DefaultTextBlockStyle(
|
|
||||||
defaultTextStyle.style.copyWith(
|
|
||||||
fontSize: 20,
|
|
||||||
height: 1.5,
|
|
||||||
color: Colors.grey.withOpacity(0.6),
|
|
||||||
),
|
|
||||||
const Tuple2(0, 0),
|
|
||||||
const Tuple2(0, 0),
|
|
||||||
null),
|
|
||||||
lists: DefaultListBlockStyle(baseStyle, baseSpacing, const Tuple2(0, 6),
|
|
||||||
null, StyleWidgetBuilder.checkbox(theme)),
|
|
||||||
quote: DefaultTextBlockStyle(
|
|
||||||
TextStyle(color: baseStyle.color!.withOpacity(0.6)),
|
|
||||||
baseSpacing,
|
|
||||||
const Tuple2(6, 2),
|
|
||||||
BoxDecoration(
|
|
||||||
border: Border(
|
|
||||||
left: BorderSide(width: 4, color: theme.shader5),
|
|
||||||
),
|
|
||||||
)),
|
|
||||||
code: DefaultTextBlockStyle(
|
|
||||||
TextStyle(
|
|
||||||
color: Colors.blue.shade900.withOpacity(0.9),
|
|
||||||
fontFamily: fontFamily,
|
|
||||||
fontSize: 13,
|
|
||||||
height: 1.15,
|
|
||||||
),
|
|
||||||
baseSpacing,
|
|
||||||
const Tuple2(0, 0),
|
|
||||||
BoxDecoration(
|
|
||||||
color: Colors.grey.shade50,
|
|
||||||
borderRadius: BorderRadius.circular(2),
|
|
||||||
)),
|
|
||||||
indent: DefaultTextBlockStyle(
|
|
||||||
baseStyle, baseSpacing, const Tuple2(0, 6), null),
|
|
||||||
align: DefaultTextBlockStyle(
|
|
||||||
baseStyle, const Tuple2(0, 0), const Tuple2(0, 0), null),
|
|
||||||
leading: DefaultTextBlockStyle(
|
|
||||||
baseStyle, const Tuple2(0, 0), const Tuple2(0, 0), null),
|
|
||||||
sizeSmall: const TextStyle(fontSize: 10),
|
|
||||||
sizeLarge: const TextStyle(fontSize: 18),
|
|
||||||
sizeHuge: const TextStyle(fontSize: 22));
|
|
||||||
}
|
|
||||||
|
|
||||||
String makeFontFamily(ThemeData themeData) {
|
|
||||||
String fontFamily;
|
|
||||||
switch (themeData.platform) {
|
|
||||||
case TargetPlatform.iOS:
|
|
||||||
case TargetPlatform.macOS:
|
|
||||||
fontFamily = 'Mulish';
|
|
||||||
break;
|
|
||||||
case TargetPlatform.android:
|
|
||||||
case TargetPlatform.fuchsia:
|
|
||||||
case TargetPlatform.windows:
|
|
||||||
case TargetPlatform.linux:
|
|
||||||
fontFamily = 'Roboto Mono';
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw UnimplementedError();
|
|
||||||
}
|
|
||||||
return fontFamily;
|
|
||||||
}
|
|
@ -12,9 +12,7 @@ import 'package:flowy_sdk/protobuf/flowy-folder/protobuf.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class AuthRouter {
|
class AuthRouter {
|
||||||
void pushForgetPasswordScreen(BuildContext context) {
|
void pushForgetPasswordScreen(BuildContext context) {}
|
||||||
// TODO: implement showForgetPasswordScreen
|
|
||||||
}
|
|
||||||
|
|
||||||
void pushWelcomeScreen(BuildContext context, UserProfilePB userProfile) {
|
void pushWelcomeScreen(BuildContext context, UserProfilePB userProfile) {
|
||||||
getIt<SplashRoute>().pushWelcomeScreen(context, userProfile);
|
getIt<SplashRoute>().pushWelcomeScreen(context, userProfile);
|
||||||
|
@ -1,30 +0,0 @@
|
|||||||
library delta_markdown;
|
|
||||||
|
|
||||||
import 'dart:convert';
|
|
||||||
|
|
||||||
import 'src/delta_markdown_decoder.dart';
|
|
||||||
import 'src/delta_markdown_encoder.dart';
|
|
||||||
import 'src/version.dart';
|
|
||||||
|
|
||||||
const version = packageVersion;
|
|
||||||
|
|
||||||
/// Codec used to convert between Markdown and Quill deltas.
|
|
||||||
const DeltaMarkdownCodec _kCodec = DeltaMarkdownCodec();
|
|
||||||
|
|
||||||
String markdownToDelta(String markdown) {
|
|
||||||
return _kCodec.decode(markdown);
|
|
||||||
}
|
|
||||||
|
|
||||||
String deltaToMarkdown(String delta) {
|
|
||||||
return _kCodec.encode(delta);
|
|
||||||
}
|
|
||||||
|
|
||||||
class DeltaMarkdownCodec extends Codec<String, String> {
|
|
||||||
const DeltaMarkdownCodec();
|
|
||||||
|
|
||||||
@override
|
|
||||||
Converter<String, String> get decoder => DeltaMarkdownDecoder();
|
|
||||||
|
|
||||||
@override
|
|
||||||
Converter<String, String> get encoder => DeltaMarkdownEncoder();
|
|
||||||
}
|
|
@ -1,113 +0,0 @@
|
|||||||
// Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file
|
|
||||||
// for details. All rights reserved. Use of this source code is governed by a
|
|
||||||
// BSD-style license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
typedef Resolver = Node? Function(String name, [String? title]);
|
|
||||||
|
|
||||||
/// Base class for any AST item.
|
|
||||||
///
|
|
||||||
/// Roughly corresponds to Node in the DOM. Will be either an Element or Text.
|
|
||||||
class Node {
|
|
||||||
void accept(NodeVisitor visitor) {}
|
|
||||||
|
|
||||||
bool isToplevel = false;
|
|
||||||
|
|
||||||
String? get textContent {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A named tag that can contain other nodes.
|
|
||||||
class Element extends Node {
|
|
||||||
/// Instantiates a [tag] Element with [children].
|
|
||||||
Element(this.tag, this.children) : attributes = <String, String>{};
|
|
||||||
|
|
||||||
/// Instantiates an empty, self-closing [tag] Element.
|
|
||||||
Element.empty(this.tag)
|
|
||||||
: children = null,
|
|
||||||
attributes = {};
|
|
||||||
|
|
||||||
/// Instantiates a [tag] Element with no [children].
|
|
||||||
Element.withTag(this.tag)
|
|
||||||
: children = [],
|
|
||||||
attributes = {};
|
|
||||||
|
|
||||||
/// Instantiates a [tag] Element with a single Text child.
|
|
||||||
Element.text(this.tag, String text)
|
|
||||||
: children = [Text(text)],
|
|
||||||
attributes = {};
|
|
||||||
|
|
||||||
final String tag;
|
|
||||||
final List<Node>? children;
|
|
||||||
final Map<String, String> attributes;
|
|
||||||
String? generatedId;
|
|
||||||
|
|
||||||
/// Whether this element is self-closing.
|
|
||||||
bool get isEmpty => children == null;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void accept(NodeVisitor visitor) {
|
|
||||||
if (visitor.visitElementBefore(this)) {
|
|
||||||
if (children != null) {
|
|
||||||
for (final child in children!) {
|
|
||||||
child.accept(visitor);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
visitor.visitElementAfter(this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get textContent => children == null
|
|
||||||
? ''
|
|
||||||
: children!.map((child) => child.textContent).join();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A plain text element.
|
|
||||||
class Text extends Node {
|
|
||||||
Text(this.text);
|
|
||||||
|
|
||||||
final String text;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void accept(NodeVisitor visitor) => visitor.visitText(this);
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get textContent => text;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Inline content that has not been parsed into inline nodes (strong, links,
|
|
||||||
/// etc).
|
|
||||||
///
|
|
||||||
/// These placeholder nodes should only remain in place while the block nodes
|
|
||||||
/// of a document are still being parsed, in order to gather all reference link
|
|
||||||
/// definitions.
|
|
||||||
class UnparsedContent extends Node {
|
|
||||||
UnparsedContent(this.textContent);
|
|
||||||
|
|
||||||
@override
|
|
||||||
final String textContent;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void accept(NodeVisitor visitor);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Visitor pattern for the AST.
|
|
||||||
///
|
|
||||||
/// Renderers or other AST transformers should implement this.
|
|
||||||
abstract class NodeVisitor {
|
|
||||||
/// Called when a Text node has been reached.
|
|
||||||
void visitText(Text text);
|
|
||||||
|
|
||||||
/// Called when an Element has been reached, before its children have been
|
|
||||||
/// visited.
|
|
||||||
///
|
|
||||||
/// Returns `false` to skip its children.
|
|
||||||
bool visitElementBefore(Element element);
|
|
||||||
|
|
||||||
/// Called when an Element has been reached, after its children have been
|
|
||||||
/// visited.
|
|
||||||
///
|
|
||||||
/// Will not be called if [visitElementBefore] returns `false`.
|
|
||||||
void visitElementAfter(Element element);
|
|
||||||
}
|
|
File diff suppressed because it is too large
Load Diff
@ -1,255 +0,0 @@
|
|||||||
import 'dart:collection';
|
|
||||||
import 'dart:convert';
|
|
||||||
|
|
||||||
import 'package:flutter_quill/models/documents/attribute.dart';
|
|
||||||
import 'package:flutter_quill/models/quill_delta.dart';
|
|
||||||
|
|
||||||
import 'ast.dart' as ast;
|
|
||||||
import 'document.dart';
|
|
||||||
|
|
||||||
class DeltaMarkdownDecoder extends Converter<String, String> {
|
|
||||||
@override
|
|
||||||
String convert(String input) {
|
|
||||||
final lines = input.replaceAll('\r\n', '\n').split('\n');
|
|
||||||
|
|
||||||
final markdownDocument = Document().parseLines(lines);
|
|
||||||
|
|
||||||
return jsonEncode(_DeltaVisitor().convert(markdownDocument).toJson());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _DeltaVisitor implements ast.NodeVisitor {
|
|
||||||
static final _blockTags =
|
|
||||||
RegExp('h1|h2|h3|h4|h5|h6|hr|pre|ul|ol|blockquote|p|pre');
|
|
||||||
|
|
||||||
static final _embedTags = RegExp('hr|img');
|
|
||||||
|
|
||||||
late Delta delta;
|
|
||||||
|
|
||||||
late Queue<Attribute> activeInlineAttributes;
|
|
||||||
Attribute? activeBlockAttribute;
|
|
||||||
late Set<String> uniqueIds;
|
|
||||||
|
|
||||||
ast.Element? previousElement;
|
|
||||||
late ast.Element previousToplevelElement;
|
|
||||||
|
|
||||||
Delta convert(List<ast.Node> nodes) {
|
|
||||||
delta = Delta();
|
|
||||||
activeInlineAttributes = Queue<Attribute>();
|
|
||||||
uniqueIds = <String>{};
|
|
||||||
|
|
||||||
for (final node in nodes) {
|
|
||||||
node.accept(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure the delta ends with a newline.
|
|
||||||
if (delta.length > 0 && delta.last.value != '\n') {
|
|
||||||
delta.insert('\n', activeBlockAttribute?.toJson());
|
|
||||||
}
|
|
||||||
|
|
||||||
return delta;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void visitText(ast.Text text) {
|
|
||||||
// Remove trailing newline
|
|
||||||
//final lines = text.text.trim().split('\n');
|
|
||||||
|
|
||||||
/*
|
|
||||||
final attributes = Map<String, dynamic>();
|
|
||||||
for (final attr in activeInlineAttributes) {
|
|
||||||
attributes.addAll(attr.toJson());
|
|
||||||
}
|
|
||||||
|
|
||||||
for (final l in lines) {
|
|
||||||
delta.insert(l, attributes);
|
|
||||||
delta.insert('\n', activeBlockAttribute.toJson());
|
|
||||||
}*/
|
|
||||||
|
|
||||||
final str = text.text;
|
|
||||||
//if (str.endsWith('\n')) str = str.substring(0, str.length - 1);
|
|
||||||
|
|
||||||
final attributes = <String, dynamic>{};
|
|
||||||
for (final attr in activeInlineAttributes) {
|
|
||||||
attributes.addAll(attr.toJson());
|
|
||||||
}
|
|
||||||
|
|
||||||
var newlineIndex = str.indexOf('\n');
|
|
||||||
var startIndex = 0;
|
|
||||||
while (newlineIndex != -1) {
|
|
||||||
final previousText = str.substring(startIndex, newlineIndex);
|
|
||||||
if (previousText.isNotEmpty) {
|
|
||||||
delta.insert(previousText, attributes.isNotEmpty ? attributes : null);
|
|
||||||
}
|
|
||||||
delta.insert('\n', activeBlockAttribute?.toJson());
|
|
||||||
|
|
||||||
startIndex = newlineIndex + 1;
|
|
||||||
newlineIndex = str.indexOf('\n', newlineIndex + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (startIndex < str.length) {
|
|
||||||
final lastStr = str.substring(startIndex);
|
|
||||||
delta.insert(lastStr, attributes.isNotEmpty ? attributes : null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool visitElementBefore(ast.Element element) {
|
|
||||||
// Hackish. Separate block-level elements with newlines.
|
|
||||||
final attr = _tagToAttribute(element);
|
|
||||||
|
|
||||||
if (delta.isNotEmpty && _blockTags.firstMatch(element.tag) != null) {
|
|
||||||
if (element.isToplevel) {
|
|
||||||
// If the last active block attribute is not a list, we need to finish
|
|
||||||
// it off.
|
|
||||||
if (previousToplevelElement.tag != 'ul' &&
|
|
||||||
previousToplevelElement.tag != 'ol' &&
|
|
||||||
previousToplevelElement.tag != 'pre' &&
|
|
||||||
previousToplevelElement.tag != 'hr') {
|
|
||||||
delta.insert('\n', activeBlockAttribute?.toJson());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only separate the blocks if both are paragraphs.
|
|
||||||
//
|
|
||||||
// TODO(kolja): Determine which behavior we really want here.
|
|
||||||
// We can either insert an additional newline or just have the
|
|
||||||
// paragraphs as single lines. Zefyr will by default render two lines
|
|
||||||
// are different paragraphs so for now we will not add an additional
|
|
||||||
// newline here.
|
|
||||||
//
|
|
||||||
// if (previousToplevelElement != null &&
|
|
||||||
// previousToplevelElement.tag == 'p' &&
|
|
||||||
// element.tag == 'p') {
|
|
||||||
// delta.insert('\n');
|
|
||||||
// }
|
|
||||||
} else if (element.tag == 'p' &&
|
|
||||||
previousElement != null &&
|
|
||||||
!previousElement!.isToplevel &&
|
|
||||||
!previousElement!.children!.contains(element)) {
|
|
||||||
// Here we have two children of the same toplevel element. These need
|
|
||||||
// to be separated by additional newlines.
|
|
||||||
|
|
||||||
delta
|
|
||||||
// Finish off the last lower-level block.
|
|
||||||
..insert('\n', activeBlockAttribute?.toJson())
|
|
||||||
// Add an empty line between the lower-level blocks.
|
|
||||||
..insert('\n', activeBlockAttribute?.toJson());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Keep track of the top-level block attribute.
|
|
||||||
if (element.isToplevel && element.tag != 'hr') {
|
|
||||||
// Hacky solution for horizontal rule so that the attribute is not added
|
|
||||||
// to the line feed at the end of the line.
|
|
||||||
activeBlockAttribute = attr;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_embedTags.firstMatch(element.tag) != null) {
|
|
||||||
// We write out the element here since the embed has no children or
|
|
||||||
// content.
|
|
||||||
delta.insert(attr!.toJson());
|
|
||||||
} else if (_blockTags.firstMatch(element.tag) == null && attr != null) {
|
|
||||||
activeInlineAttributes.addLast(attr);
|
|
||||||
}
|
|
||||||
|
|
||||||
previousElement = element;
|
|
||||||
if (element.isToplevel) {
|
|
||||||
previousToplevelElement = element;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (element.isEmpty) {
|
|
||||||
// Empty element like <hr/>.
|
|
||||||
//buffer.write(' />');
|
|
||||||
|
|
||||||
if (element.tag == 'br') {
|
|
||||||
delta.insert('\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
} else {
|
|
||||||
//buffer.write('>');
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void visitElementAfter(ast.Element element) {
|
|
||||||
if (element.tag == 'li' &&
|
|
||||||
(previousToplevelElement.tag == 'ol' ||
|
|
||||||
previousToplevelElement.tag == 'ul')) {
|
|
||||||
delta.insert('\n', activeBlockAttribute?.toJson());
|
|
||||||
}
|
|
||||||
|
|
||||||
final attr = _tagToAttribute(element);
|
|
||||||
if (attr == null || !attr.isInline || activeInlineAttributes.last != attr) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
activeInlineAttributes.removeLast();
|
|
||||||
|
|
||||||
// Always keep track of the last element.
|
|
||||||
// This becomes relevant if we have something like
|
|
||||||
//
|
|
||||||
// <ul>
|
|
||||||
// <li>...</li>
|
|
||||||
// <li>...</li>
|
|
||||||
// </ul>
|
|
||||||
previousElement = element;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Uniquifies an id generated from text.
|
|
||||||
String uniquifyId(String id) {
|
|
||||||
if (!uniqueIds.contains(id)) {
|
|
||||||
uniqueIds.add(id);
|
|
||||||
return id;
|
|
||||||
}
|
|
||||||
|
|
||||||
var suffix = 2;
|
|
||||||
var suffixedId = '$id-$suffix';
|
|
||||||
while (uniqueIds.contains(suffixedId)) {
|
|
||||||
suffixedId = '$id-${suffix++}';
|
|
||||||
}
|
|
||||||
uniqueIds.add(suffixedId);
|
|
||||||
return suffixedId;
|
|
||||||
}
|
|
||||||
|
|
||||||
Attribute? _tagToAttribute(ast.Element el) {
|
|
||||||
switch (el.tag) {
|
|
||||||
case 'em':
|
|
||||||
return Attribute.italic;
|
|
||||||
case 'strong':
|
|
||||||
return Attribute.bold;
|
|
||||||
case 'ul':
|
|
||||||
return Attribute.ul;
|
|
||||||
case 'ol':
|
|
||||||
return Attribute.ol;
|
|
||||||
case 'pre':
|
|
||||||
return Attribute.codeBlock;
|
|
||||||
case 'blockquote':
|
|
||||||
return Attribute.blockQuote;
|
|
||||||
case 'h1':
|
|
||||||
return Attribute.h1;
|
|
||||||
case 'h2':
|
|
||||||
return Attribute.h2;
|
|
||||||
case 'h3':
|
|
||||||
return Attribute.h3;
|
|
||||||
case 'a':
|
|
||||||
final href = el.attributes['href'];
|
|
||||||
return LinkAttribute(href);
|
|
||||||
case 'img':
|
|
||||||
final href = el.attributes['src'];
|
|
||||||
return ImageAttribute(href);
|
|
||||||
case 'hr':
|
|
||||||
return DividerAttribute();
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class ImageAttribute extends Attribute<String?> {
|
|
||||||
ImageAttribute(String? val) : super('image', AttributeScope.EMBEDS, val);
|
|
||||||
}
|
|
||||||
|
|
||||||
class DividerAttribute extends Attribute<String?> {
|
|
||||||
DividerAttribute() : super('divider', AttributeScope.EMBEDS, 'hr');
|
|
||||||
}
|
|
@ -1,284 +0,0 @@
|
|||||||
import 'dart:convert';
|
|
||||||
|
|
||||||
import 'package:collection/collection.dart' show IterableExtension;
|
|
||||||
import 'package:flutter_quill/models/documents/attribute.dart';
|
|
||||||
import 'package:flutter_quill/models/documents/nodes/embed.dart';
|
|
||||||
import 'package:flutter_quill/models/documents/style.dart';
|
|
||||||
import 'package:flutter_quill/models/quill_delta.dart';
|
|
||||||
|
|
||||||
class DeltaMarkdownEncoder extends Converter<String, String> {
|
|
||||||
static const _lineFeedAsciiCode = 0x0A;
|
|
||||||
|
|
||||||
late StringBuffer markdownBuffer;
|
|
||||||
late StringBuffer lineBuffer;
|
|
||||||
|
|
||||||
Attribute? currentBlockStyle;
|
|
||||||
late Style currentInlineStyle;
|
|
||||||
|
|
||||||
late List<String> currentBlockLines;
|
|
||||||
|
|
||||||
/// Converts the [input] delta to Markdown.
|
|
||||||
@override
|
|
||||||
String convert(String input) {
|
|
||||||
markdownBuffer = StringBuffer();
|
|
||||||
lineBuffer = StringBuffer();
|
|
||||||
currentInlineStyle = Style();
|
|
||||||
currentBlockLines = <String>[];
|
|
||||||
|
|
||||||
final inputJson = jsonDecode(input) as List<dynamic>?;
|
|
||||||
if (inputJson is! List<dynamic>) {
|
|
||||||
throw ArgumentError('Unexpected formatting of the input delta string.');
|
|
||||||
}
|
|
||||||
final delta = Delta.fromJson(inputJson);
|
|
||||||
final iterator = DeltaIterator(delta);
|
|
||||||
|
|
||||||
while (iterator.hasNext) {
|
|
||||||
final operation = iterator.next();
|
|
||||||
|
|
||||||
if (operation.data is String) {
|
|
||||||
final operationData = operation.data as String;
|
|
||||||
|
|
||||||
if (!operationData.contains('\n')) {
|
|
||||||
_handleInline(lineBuffer, operationData, operation.attributes);
|
|
||||||
} else {
|
|
||||||
_handleLine(operationData, operation.attributes);
|
|
||||||
}
|
|
||||||
} else if (operation.data is Map<String, dynamic>) {
|
|
||||||
_handleEmbed(operation.data as Map<String, dynamic>);
|
|
||||||
} else {
|
|
||||||
throw ArgumentError('Unexpected formatting of the input delta string.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_handleBlock(currentBlockStyle); // Close the last block
|
|
||||||
|
|
||||||
return markdownBuffer.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
void _handleInline(
|
|
||||||
StringBuffer buffer,
|
|
||||||
String text,
|
|
||||||
Map<String, dynamic>? attributes,
|
|
||||||
) {
|
|
||||||
final style = Style.fromJson(attributes);
|
|
||||||
|
|
||||||
// First close any current styles if needed
|
|
||||||
final markedForRemoval = <Attribute>[];
|
|
||||||
// Close the styles in reverse order, e.g. **_ for _**Test**_.
|
|
||||||
for (final value
|
|
||||||
in currentInlineStyle.attributes.values.toList().reversed) {
|
|
||||||
// TODO(tillf): Is block correct?
|
|
||||||
if (value.scope == AttributeScope.BLOCK) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (style.containsKey(value.key)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
final padding = _trimRight(buffer);
|
|
||||||
_writeAttribute(buffer, value, close: true);
|
|
||||||
if (padding.isNotEmpty) {
|
|
||||||
buffer.write(padding);
|
|
||||||
}
|
|
||||||
markedForRemoval.add(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make sure to remove all attributes that are marked for removal.
|
|
||||||
for (final value in markedForRemoval) {
|
|
||||||
currentInlineStyle.attributes.removeWhere((_, v) => v == value);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now open any new styles.
|
|
||||||
for (final attribute in style.attributes.values) {
|
|
||||||
// TODO(tillf): Is block correct?
|
|
||||||
if (attribute.scope == AttributeScope.BLOCK) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (currentInlineStyle.containsKey(attribute.key)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
final originalText = text;
|
|
||||||
text = text.trimLeft();
|
|
||||||
final padding = ' ' * (originalText.length - text.length);
|
|
||||||
if (padding.isNotEmpty) {
|
|
||||||
buffer.write(padding);
|
|
||||||
}
|
|
||||||
_writeAttribute(buffer, attribute);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write the text itself
|
|
||||||
buffer.write(text);
|
|
||||||
currentInlineStyle = style;
|
|
||||||
}
|
|
||||||
|
|
||||||
void _handleLine(String data, Map<String, dynamic>? attributes) {
|
|
||||||
final span = StringBuffer();
|
|
||||||
|
|
||||||
for (var i = 0; i < data.length; i++) {
|
|
||||||
if (data.codeUnitAt(i) == _lineFeedAsciiCode) {
|
|
||||||
if (span.isNotEmpty) {
|
|
||||||
// Write the span if it's not empty.
|
|
||||||
_handleInline(lineBuffer, span.toString(), attributes);
|
|
||||||
}
|
|
||||||
// Close any open inline styles.
|
|
||||||
_handleInline(lineBuffer, '', null);
|
|
||||||
|
|
||||||
final lineBlock = Style.fromJson(attributes)
|
|
||||||
.attributes
|
|
||||||
.values
|
|
||||||
.singleWhereOrNull((a) => a.scope == AttributeScope.BLOCK);
|
|
||||||
|
|
||||||
if (lineBlock == currentBlockStyle) {
|
|
||||||
currentBlockLines.add(lineBuffer.toString());
|
|
||||||
} else {
|
|
||||||
_handleBlock(currentBlockStyle);
|
|
||||||
currentBlockLines
|
|
||||||
..clear()
|
|
||||||
..add(lineBuffer.toString());
|
|
||||||
|
|
||||||
currentBlockStyle = lineBlock;
|
|
||||||
}
|
|
||||||
lineBuffer.clear();
|
|
||||||
|
|
||||||
span.clear();
|
|
||||||
} else {
|
|
||||||
span.writeCharCode(data.codeUnitAt(i));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remaining span
|
|
||||||
if (span.isNotEmpty) {
|
|
||||||
_handleInline(lineBuffer, span.toString(), attributes);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _handleEmbed(Map<String, dynamic> data) {
|
|
||||||
final embed = BlockEmbed(data.keys.first, data.values.first as String);
|
|
||||||
|
|
||||||
if (embed.type == 'image') {
|
|
||||||
_writeEmbedTag(lineBuffer, embed);
|
|
||||||
_writeEmbedTag(lineBuffer, embed, close: true);
|
|
||||||
} else if (embed.type == 'divider') {
|
|
||||||
_writeEmbedTag(lineBuffer, embed);
|
|
||||||
_writeEmbedTag(lineBuffer, embed, close: true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _handleBlock(Attribute? blockStyle) {
|
|
||||||
if (currentBlockLines.isEmpty) {
|
|
||||||
return; // Empty block
|
|
||||||
}
|
|
||||||
|
|
||||||
// If there was a block before this one, add empty line between the blocks
|
|
||||||
if (markdownBuffer.isNotEmpty) {
|
|
||||||
markdownBuffer.writeln();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (blockStyle == null) {
|
|
||||||
markdownBuffer
|
|
||||||
..write(currentBlockLines.join('\n'))
|
|
||||||
..writeln();
|
|
||||||
} else if (blockStyle == Attribute.codeBlock) {
|
|
||||||
_writeAttribute(markdownBuffer, blockStyle);
|
|
||||||
markdownBuffer.write(currentBlockLines.join('\n'));
|
|
||||||
_writeAttribute(markdownBuffer, blockStyle, close: true);
|
|
||||||
markdownBuffer.writeln();
|
|
||||||
} else {
|
|
||||||
// Dealing with lists or a quote.
|
|
||||||
for (final line in currentBlockLines) {
|
|
||||||
_writeBlockTag(markdownBuffer, blockStyle);
|
|
||||||
markdownBuffer
|
|
||||||
..write(line)
|
|
||||||
..writeln();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
String _trimRight(StringBuffer buffer) {
|
|
||||||
final text = buffer.toString();
|
|
||||||
if (!text.endsWith(' ')) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
final result = text.trimRight();
|
|
||||||
buffer
|
|
||||||
..clear()
|
|
||||||
..write(result);
|
|
||||||
return ' ' * (text.length - result.length);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _writeAttribute(
|
|
||||||
StringBuffer buffer,
|
|
||||||
Attribute attribute, {
|
|
||||||
bool close = false,
|
|
||||||
}) {
|
|
||||||
if (attribute.key == Attribute.bold.key) {
|
|
||||||
buffer.write('**');
|
|
||||||
} else if (attribute.key == Attribute.italic.key) {
|
|
||||||
buffer.write('_');
|
|
||||||
} else if (attribute.key == Attribute.link.key) {
|
|
||||||
buffer.write(!close ? '[' : '](${attribute.value})');
|
|
||||||
} else if (attribute == Attribute.codeBlock) {
|
|
||||||
buffer.write(!close ? '```\n' : '\n```');
|
|
||||||
} else if (attribute.key == Attribute.background.key) {
|
|
||||||
buffer.write(!close ? '<mark>' : '</mark>');
|
|
||||||
} else if (attribute.key == Attribute.underline.key) {
|
|
||||||
buffer.write(!close ? '<u>' : '</u>');
|
|
||||||
} else if (attribute.key == Attribute.codeBlock.key) {
|
|
||||||
buffer.write(!close ? '```\n' : '\n```');
|
|
||||||
} else if (attribute.key == Attribute.inlineCode.key) {
|
|
||||||
buffer.write(!close ? '`' : '`');
|
|
||||||
} else if (attribute.key == Attribute.strikeThrough.key) {
|
|
||||||
buffer.write(!close ? '~~' : '~~');
|
|
||||||
} else {
|
|
||||||
// do nothing, just skip the unknown attribute.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _writeBlockTag(
|
|
||||||
StringBuffer buffer,
|
|
||||||
Attribute block, {
|
|
||||||
bool close = false,
|
|
||||||
}) {
|
|
||||||
if (close) {
|
|
||||||
return; // no close tag needed for simple blocks.
|
|
||||||
}
|
|
||||||
|
|
||||||
if (block == Attribute.blockQuote) {
|
|
||||||
buffer.write('> ');
|
|
||||||
} else if (block == Attribute.ul) {
|
|
||||||
buffer.write('* ');
|
|
||||||
} else if (block == Attribute.ol) {
|
|
||||||
buffer.write('1. ');
|
|
||||||
} else if (block.key == Attribute.h1.key && block.value == 1) {
|
|
||||||
buffer.write('# ');
|
|
||||||
} else if (block.key == Attribute.h2.key && block.value == 2) {
|
|
||||||
buffer.write('## ');
|
|
||||||
} else if (block.key == Attribute.h3.key && block.value == 3) {
|
|
||||||
buffer.write('### ');
|
|
||||||
} else if (block.key == Attribute.list.key) {
|
|
||||||
buffer.write('* ');
|
|
||||||
} else {
|
|
||||||
// do nothing, just skip the unknown attribute.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _writeEmbedTag(
|
|
||||||
StringBuffer buffer,
|
|
||||||
BlockEmbed embed, {
|
|
||||||
bool close = false,
|
|
||||||
}) {
|
|
||||||
const kImageType = 'image';
|
|
||||||
const kDividerType = 'divider';
|
|
||||||
|
|
||||||
if (embed.type == kImageType) {
|
|
||||||
if (close) {
|
|
||||||
buffer.write('](${embed.data})');
|
|
||||||
} else {
|
|
||||||
buffer.write('![');
|
|
||||||
}
|
|
||||||
} else if (embed.type == kDividerType && close) {
|
|
||||||
buffer.write('\n---\n\n');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,88 +0,0 @@
|
|||||||
// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file
|
|
||||||
// for details. All rights reserved. Use of this source code is governed by a
|
|
||||||
// BSD-style license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
import 'ast.dart';
|
|
||||||
import 'block_parser.dart';
|
|
||||||
import 'extension_set.dart';
|
|
||||||
import 'inline_parser.dart';
|
|
||||||
|
|
||||||
/// Maintains the context needed to parse a Markdown document.
|
|
||||||
class Document {
|
|
||||||
Document({
|
|
||||||
Iterable<BlockSyntax>? blockSyntaxes,
|
|
||||||
Iterable<InlineSyntax>? inlineSyntaxes,
|
|
||||||
ExtensionSet? extensionSet,
|
|
||||||
this.linkResolver,
|
|
||||||
this.imageLinkResolver,
|
|
||||||
}) : extensionSet = extensionSet ?? ExtensionSet.commonMark {
|
|
||||||
_blockSyntaxes
|
|
||||||
..addAll(blockSyntaxes ?? [])
|
|
||||||
..addAll(this.extensionSet.blockSyntaxes);
|
|
||||||
_inlineSyntaxes
|
|
||||||
..addAll(inlineSyntaxes ?? [])
|
|
||||||
..addAll(this.extensionSet.inlineSyntaxes);
|
|
||||||
}
|
|
||||||
|
|
||||||
final Map<String, LinkReference> linkReferences = <String, LinkReference>{};
|
|
||||||
final ExtensionSet extensionSet;
|
|
||||||
final Resolver? linkResolver;
|
|
||||||
final Resolver? imageLinkResolver;
|
|
||||||
final _blockSyntaxes = <BlockSyntax>{};
|
|
||||||
final _inlineSyntaxes = <InlineSyntax>{};
|
|
||||||
|
|
||||||
Iterable<BlockSyntax> get blockSyntaxes => _blockSyntaxes;
|
|
||||||
Iterable<InlineSyntax> get inlineSyntaxes => _inlineSyntaxes;
|
|
||||||
|
|
||||||
/// Parses the given [lines] of Markdown to a series of AST nodes.
|
|
||||||
List<Node> parseLines(List<String> lines) {
|
|
||||||
final nodes = BlockParser(lines, this).parseLines();
|
|
||||||
// Make sure to mark the top level nodes as such.
|
|
||||||
for (final n in nodes) {
|
|
||||||
n.isToplevel = true;
|
|
||||||
}
|
|
||||||
_parseInlineContent(nodes);
|
|
||||||
return nodes;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Parses the given inline Markdown [text] to a series of AST nodes.
|
|
||||||
List<Node>? parseInline(String text) => InlineParser(text, this).parse();
|
|
||||||
|
|
||||||
void _parseInlineContent(List<Node> nodes) {
|
|
||||||
for (var i = 0; i < nodes.length; i++) {
|
|
||||||
final node = nodes[i];
|
|
||||||
if (node is UnparsedContent) {
|
|
||||||
final inlineNodes = parseInline(node.textContent)!;
|
|
||||||
nodes
|
|
||||||
..removeAt(i)
|
|
||||||
..insertAll(i, inlineNodes);
|
|
||||||
i += inlineNodes.length - 1;
|
|
||||||
} else if (node is Element && node.children != null) {
|
|
||||||
_parseInlineContent(node.children!);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A [link reference
|
|
||||||
/// definition](http://spec.commonmark.org/0.28/#link-reference-definitions).
|
|
||||||
class LinkReference {
|
|
||||||
/// Construct a [LinkReference], with all necessary fields.
|
|
||||||
///
|
|
||||||
/// If the parsed link reference definition does not include a title, use
|
|
||||||
/// `null` for the [title] parameter.
|
|
||||||
LinkReference(this.label, this.destination, this.title);
|
|
||||||
|
|
||||||
/// The [link label](http://spec.commonmark.org/0.28/#link-label).
|
|
||||||
///
|
|
||||||
/// Temporarily, this class is also being used to represent the link data for
|
|
||||||
/// an inline link (the destination and title), but this should change before
|
|
||||||
/// the package is released.
|
|
||||||
final String label;
|
|
||||||
|
|
||||||
/// The [link destination](http://spec.commonmark.org/0.28/#link-destination).
|
|
||||||
final String destination;
|
|
||||||
|
|
||||||
/// The [link title](http://spec.commonmark.org/0.28/#link-title).
|
|
||||||
final String title;
|
|
||||||
}
|
|
File diff suppressed because it is too large
Load Diff
@ -1,64 +0,0 @@
|
|||||||
import 'block_parser.dart';
|
|
||||||
import 'inline_parser.dart';
|
|
||||||
|
|
||||||
/// ExtensionSets provide a simple grouping mechanism for common Markdown
|
|
||||||
/// flavors.
|
|
||||||
///
|
|
||||||
/// For example, the [gitHubFlavored] set of syntax extensions allows users to
|
|
||||||
/// output HTML from their Markdown in a similar fashion to GitHub's parsing.
|
|
||||||
class ExtensionSet {
|
|
||||||
ExtensionSet(this.blockSyntaxes, this.inlineSyntaxes);
|
|
||||||
|
|
||||||
/// The [ExtensionSet.none] extension set renders Markdown similar to
|
|
||||||
/// [Markdown.pl].
|
|
||||||
///
|
|
||||||
/// However, this set does not render _exactly_ the same as Markdown.pl;
|
|
||||||
/// rather it is more-or-less the CommonMark standard of Markdown, without
|
|
||||||
/// fenced code blocks, or inline HTML.
|
|
||||||
///
|
|
||||||
/// [Markdown.pl]: http://daringfireball.net/projects/markdown/syntax
|
|
||||||
static final ExtensionSet none = ExtensionSet([], []);
|
|
||||||
|
|
||||||
/// The [commonMark] extension set is close to compliance with [CommonMark].
|
|
||||||
///
|
|
||||||
/// [CommonMark]: http://commonmark.org/
|
|
||||||
static final ExtensionSet commonMark =
|
|
||||||
ExtensionSet([const FencedCodeBlockSyntax()], [InlineHtmlSyntax()]);
|
|
||||||
|
|
||||||
/// The [gitHubWeb] extension set renders Markdown similarly to GitHub.
|
|
||||||
///
|
|
||||||
/// This is different from the [gitHubFlavored] extension set in that GitHub
|
|
||||||
/// actually renders HTML different from straight [GitHub flavored Markdown].
|
|
||||||
///
|
|
||||||
/// (The only difference currently is that [gitHubWeb] renders headers with
|
|
||||||
/// linkable IDs.)
|
|
||||||
///
|
|
||||||
/// [GitHub flavored Markdown]: https://github.github.com/gfm/
|
|
||||||
static final ExtensionSet gitHubWeb = ExtensionSet([
|
|
||||||
const FencedCodeBlockSyntax(),
|
|
||||||
const HeaderWithIdSyntax(),
|
|
||||||
const SetextHeaderWithIdSyntax(),
|
|
||||||
const TableSyntax()
|
|
||||||
], [
|
|
||||||
InlineHtmlSyntax(),
|
|
||||||
StrikethroughSyntax(),
|
|
||||||
EmojiSyntax(),
|
|
||||||
AutolinkExtensionSyntax(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
/// The [gitHubFlavored] extension set is close to compliance with the [GitHub
|
|
||||||
/// flavored Markdown spec].
|
|
||||||
///
|
|
||||||
/// [GitHub flavored Markdown]: https://github.github.com/gfm/
|
|
||||||
static final ExtensionSet gitHubFlavored = ExtensionSet([
|
|
||||||
const FencedCodeBlockSyntax(),
|
|
||||||
const TableSyntax()
|
|
||||||
], [
|
|
||||||
InlineHtmlSyntax(),
|
|
||||||
StrikethroughSyntax(),
|
|
||||||
AutolinkExtensionSyntax(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
final List<BlockSyntax> blockSyntaxes;
|
|
||||||
final List<InlineSyntax> inlineSyntaxes;
|
|
||||||
}
|
|
@ -1,121 +0,0 @@
|
|||||||
// Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file
|
|
||||||
// for details. All rights reserved. Use of this source code is governed by a
|
|
||||||
// BSD-style license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
import 'ast.dart';
|
|
||||||
import 'block_parser.dart';
|
|
||||||
import 'document.dart';
|
|
||||||
import 'extension_set.dart';
|
|
||||||
import 'inline_parser.dart';
|
|
||||||
|
|
||||||
/// Converts the given string of Markdown to HTML.
|
|
||||||
String markdownToHtml(String markdown,
|
|
||||||
{Iterable<BlockSyntax>? blockSyntaxes,
|
|
||||||
Iterable<InlineSyntax>? inlineSyntaxes,
|
|
||||||
ExtensionSet? extensionSet,
|
|
||||||
Resolver? linkResolver,
|
|
||||||
Resolver? imageLinkResolver,
|
|
||||||
bool inlineOnly = false}) {
|
|
||||||
final document = Document(
|
|
||||||
blockSyntaxes: blockSyntaxes,
|
|
||||||
inlineSyntaxes: inlineSyntaxes,
|
|
||||||
extensionSet: extensionSet,
|
|
||||||
linkResolver: linkResolver,
|
|
||||||
imageLinkResolver: imageLinkResolver);
|
|
||||||
|
|
||||||
if (inlineOnly) {
|
|
||||||
return renderToHtml(document.parseInline(markdown)!);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Replace windows line endings with unix line endings, and split.
|
|
||||||
final lines = markdown.replaceAll('\r\n', '\n').split('\n');
|
|
||||||
|
|
||||||
return '${renderToHtml(document.parseLines(lines))}\n';
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Renders [nodes] to HTML.
|
|
||||||
String renderToHtml(List<Node> nodes) => HtmlRenderer().render(nodes);
|
|
||||||
|
|
||||||
/// Translates a parsed AST to HTML.
|
|
||||||
class HtmlRenderer implements NodeVisitor {
|
|
||||||
HtmlRenderer();
|
|
||||||
|
|
||||||
static final _blockTags = RegExp('blockquote|h1|h2|h3|h4|h5|h6|hr|p|pre');
|
|
||||||
|
|
||||||
late StringBuffer buffer;
|
|
||||||
late Set<String> uniqueIds;
|
|
||||||
|
|
||||||
String render(List<Node> nodes) {
|
|
||||||
buffer = StringBuffer();
|
|
||||||
uniqueIds = <String>{};
|
|
||||||
|
|
||||||
for (final node in nodes) {
|
|
||||||
node.accept(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
return buffer.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void visitText(Text text) {
|
|
||||||
buffer.write(text.text);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool visitElementBefore(Element element) {
|
|
||||||
// Hackish. Separate block-level elements with newlines.
|
|
||||||
if (buffer.isNotEmpty && _blockTags.firstMatch(element.tag) != null) {
|
|
||||||
buffer.write('\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
buffer.write('<${element.tag}');
|
|
||||||
|
|
||||||
// Sort the keys so that we generate stable output.
|
|
||||||
final attributeNames = element.attributes.keys.toList()
|
|
||||||
..sort((a, b) => a.compareTo(b));
|
|
||||||
|
|
||||||
for (final name in attributeNames) {
|
|
||||||
buffer.write(' $name="${element.attributes[name]}"');
|
|
||||||
}
|
|
||||||
|
|
||||||
// attach header anchor ids generated from text
|
|
||||||
if (element.generatedId != null) {
|
|
||||||
buffer.write(' id="${uniquifyId(element.generatedId!)}"');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (element.isEmpty) {
|
|
||||||
// Empty element like <hr/>.
|
|
||||||
buffer.write(' />');
|
|
||||||
|
|
||||||
if (element.tag == 'br') {
|
|
||||||
buffer.write('\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
} else {
|
|
||||||
buffer.write('>');
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void visitElementAfter(Element element) {
|
|
||||||
buffer.write('</${element.tag}>');
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Uniquifies an id generated from text.
|
|
||||||
String uniquifyId(String id) {
|
|
||||||
if (!uniqueIds.contains(id)) {
|
|
||||||
uniqueIds.add(id);
|
|
||||||
return id;
|
|
||||||
}
|
|
||||||
|
|
||||||
var suffix = 2;
|
|
||||||
var suffixedId = '$id-$suffix';
|
|
||||||
while (uniqueIds.contains(suffixedId)) {
|
|
||||||
suffixedId = '$id-${suffix++}';
|
|
||||||
}
|
|
||||||
uniqueIds.add(suffixedId);
|
|
||||||
return suffixedId;
|
|
||||||
}
|
|
||||||
}
|
|
File diff suppressed because it is too large
Load Diff
@ -1,71 +0,0 @@
|
|||||||
import 'dart:convert';
|
|
||||||
|
|
||||||
import 'package:charcode/charcode.dart';
|
|
||||||
|
|
||||||
String escapeHtml(String html) =>
|
|
||||||
const HtmlEscape(HtmlEscapeMode.element).convert(html);
|
|
||||||
|
|
||||||
// Escape the contents of [value], so that it may be used as an HTML attribute.
|
|
||||||
|
|
||||||
// Based on http://spec.commonmark.org/0.28/#backslash-escapes.
|
|
||||||
String escapeAttribute(String value) {
|
|
||||||
final result = StringBuffer();
|
|
||||||
int ch;
|
|
||||||
for (var i = 0; i < value.codeUnits.length; i++) {
|
|
||||||
ch = value.codeUnitAt(i);
|
|
||||||
if (ch == $backslash) {
|
|
||||||
i++;
|
|
||||||
if (i == value.codeUnits.length) {
|
|
||||||
result.writeCharCode(ch);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
ch = value.codeUnitAt(i);
|
|
||||||
switch (ch) {
|
|
||||||
case $quote:
|
|
||||||
result.write('"');
|
|
||||||
break;
|
|
||||||
case $exclamation:
|
|
||||||
case $hash:
|
|
||||||
case $dollar:
|
|
||||||
case $percent:
|
|
||||||
case $ampersand:
|
|
||||||
case $apostrophe:
|
|
||||||
case $lparen:
|
|
||||||
case $rparen:
|
|
||||||
case $asterisk:
|
|
||||||
case $plus:
|
|
||||||
case $comma:
|
|
||||||
case $dash:
|
|
||||||
case $dot:
|
|
||||||
case $slash:
|
|
||||||
case $colon:
|
|
||||||
case $semicolon:
|
|
||||||
case $lt:
|
|
||||||
case $equal:
|
|
||||||
case $gt:
|
|
||||||
case $question:
|
|
||||||
case $at:
|
|
||||||
case $lbracket:
|
|
||||||
case $backslash:
|
|
||||||
case $rbracket:
|
|
||||||
case $caret:
|
|
||||||
case $underscore:
|
|
||||||
case $backquote:
|
|
||||||
case $lbrace:
|
|
||||||
case $bar:
|
|
||||||
case $rbrace:
|
|
||||||
case $tilde:
|
|
||||||
result.writeCharCode(ch);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
result.write('%5C');
|
|
||||||
result.writeCharCode(ch);
|
|
||||||
}
|
|
||||||
} else if (ch == $quote) {
|
|
||||||
result.write('%22');
|
|
||||||
} else {
|
|
||||||
result.writeCharCode(ch);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result.toString();
|
|
||||||
}
|
|
@ -1,2 +0,0 @@
|
|||||||
// Generated code. Do not modify.
|
|
||||||
const packageVersion = '0.0.2';
|
|
@ -1,21 +1,17 @@
|
|||||||
import 'package:app_flowy/plugins/doc/presentation/toolbar/toolbar_icon_button.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_quill/flutter_quill.dart';
|
|
||||||
import 'package:app_flowy/workspace/presentation/widgets/emoji_picker/emoji_picker.dart';
|
import 'package:app_flowy/workspace/presentation/widgets/emoji_picker/emoji_picker.dart';
|
||||||
|
|
||||||
class FlowyEmojiStyleButton extends StatefulWidget {
|
class FlowyEmojiStyleButton extends StatefulWidget {
|
||||||
// final Attribute attribute;
|
// final Attribute attribute;
|
||||||
final String normalIcon;
|
final String normalIcon;
|
||||||
final double iconSize;
|
// TODO: enable insert emoji in appflowy_editor
|
||||||
final QuillController controller;
|
// final QuillController controller;
|
||||||
final String tooltipText;
|
final String tooltipText;
|
||||||
|
|
||||||
const FlowyEmojiStyleButton({
|
const FlowyEmojiStyleButton({
|
||||||
// required this.attribute,
|
// required this.attribute,
|
||||||
required this.normalIcon,
|
required this.normalIcon,
|
||||||
required this.controller,
|
|
||||||
required this.tooltipText,
|
required this.tooltipText,
|
||||||
this.iconSize = defaultIconSize,
|
|
||||||
Key? key,
|
Key? key,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@ -24,7 +20,6 @@ class FlowyEmojiStyleButton extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class EmojiStyleButtonState extends State<FlowyEmojiStyleButton> {
|
class EmojiStyleButtonState extends State<FlowyEmojiStyleButton> {
|
||||||
bool _isToggled = false;
|
|
||||||
// Style get _selectionStyle => widget.controller.getSelectionStyle();
|
// Style get _selectionStyle => widget.controller.getSelectionStyle();
|
||||||
final GlobalKey emojiButtonKey = GlobalKey();
|
final GlobalKey emojiButtonKey = GlobalKey();
|
||||||
OverlayEntry? _entry;
|
OverlayEntry? _entry;
|
||||||
@ -42,14 +37,15 @@ class EmojiStyleButtonState extends State<FlowyEmojiStyleButton> {
|
|||||||
// debugPrint(MediaQuery.of(context).size.width.toString());
|
// debugPrint(MediaQuery.of(context).size.width.toString());
|
||||||
// debugPrint(MediaQuery.of(context).size.height.toString());
|
// debugPrint(MediaQuery.of(context).size.height.toString());
|
||||||
|
|
||||||
return ToolbarIconButton(
|
// return ToolbarIconButton(
|
||||||
key: emojiButtonKey,
|
// key: emojiButtonKey,
|
||||||
onPressed: _toggleAttribute,
|
// onPressed: _toggleAttribute,
|
||||||
width: widget.iconSize * kIconButtonFactor,
|
// width: widget.iconSize * kIconButtonFactor,
|
||||||
isToggled: _isToggled,
|
// isToggled: _isToggled,
|
||||||
iconName: widget.normalIcon,
|
// iconName: widget.normalIcon,
|
||||||
tooltipText: widget.tooltipText,
|
// tooltipText: widget.tooltipText,
|
||||||
);
|
// );
|
||||||
|
return Container();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -58,71 +54,34 @@ class EmojiStyleButtonState extends State<FlowyEmojiStyleButton> {
|
|||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
// @override
|
// void _toggleAttribute() {
|
||||||
// void didUpdateWidget(covariant FlowyEmojiStyleButton oldWidget) {
|
// if (_entry?.mounted ?? false) {
|
||||||
// super.didUpdateWidget(oldWidget);
|
// _entry?.remove();
|
||||||
// if (oldWidget.controller != widget.controller) {
|
// _entry = null;
|
||||||
// oldWidget.controller.removeListener(_didChangeEditingValue);
|
// setState(() => _isToggled = false);
|
||||||
// widget.controller.addListener(_didChangeEditingValue);
|
// } else {
|
||||||
// _isToggled = _getIsToggled(_selectionStyle.attributes);
|
// RenderBox box =
|
||||||
|
// emojiButtonKey.currentContext?.findRenderObject() as RenderBox;
|
||||||
|
// Offset position = box.localToGlobal(Offset.zero);
|
||||||
|
|
||||||
|
// // final window = await getWindowInfo();
|
||||||
|
|
||||||
|
// _entry = OverlayEntry(
|
||||||
|
// builder: (BuildContext context) => BuildEmojiPickerView(
|
||||||
|
// controller: widget.controller,
|
||||||
|
// offset: position,
|
||||||
|
// ),
|
||||||
|
// );
|
||||||
|
|
||||||
|
// Overlay.of(context)!.insert(_entry!);
|
||||||
|
// setState(() => _isToggled = true);
|
||||||
// }
|
// }
|
||||||
|
|
||||||
// }
|
// }
|
||||||
|
|
||||||
// @override
|
|
||||||
// void dispose() {
|
|
||||||
// widget.controller.removeListener(_didChangeEditingValue);
|
|
||||||
// super.dispose();
|
|
||||||
// }
|
|
||||||
|
|
||||||
// void _didChangeEditingValue() {
|
|
||||||
// setState(() => _isToggled = _getIsToggled(_selectionStyle.attributes));
|
|
||||||
// }
|
|
||||||
|
|
||||||
// bool _getIsToggled(Map<String, Attribute> attrs) {
|
|
||||||
// return _entry.mounted;
|
|
||||||
// }
|
|
||||||
|
|
||||||
void _toggleAttribute() {
|
|
||||||
if (_entry?.mounted ?? false) {
|
|
||||||
_entry?.remove();
|
|
||||||
_entry = null;
|
|
||||||
setState(() => _isToggled = false);
|
|
||||||
} else {
|
|
||||||
RenderBox box =
|
|
||||||
emojiButtonKey.currentContext?.findRenderObject() as RenderBox;
|
|
||||||
Offset position = box.localToGlobal(Offset.zero);
|
|
||||||
|
|
||||||
// final window = await getWindowInfo();
|
|
||||||
|
|
||||||
_entry = OverlayEntry(
|
|
||||||
builder: (BuildContext context) => BuildEmojiPickerView(
|
|
||||||
controller: widget.controller,
|
|
||||||
offset: position,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
Overlay.of(context)!.insert(_entry!);
|
|
||||||
setState(() => _isToggled = true);
|
|
||||||
}
|
|
||||||
|
|
||||||
//TODO @gaganyadav80: INFO: throws error when using TextField with FlowyOverlay.
|
|
||||||
|
|
||||||
// FlowyOverlay.of(context).insertWithRect(
|
|
||||||
// widget: BuildEmojiPickerView(controller: widget.controller),
|
|
||||||
// identifier: 'overlay_emoji_picker',
|
|
||||||
// anchorPosition: Offset(position.dx + 40, position.dy - 10),
|
|
||||||
// anchorSize: window.frame.size,
|
|
||||||
// anchorDirection: AnchorDirection.topLeft,
|
|
||||||
// style: FlowyOverlayStyle(blur: true),
|
|
||||||
// );
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class BuildEmojiPickerView extends StatefulWidget {
|
class BuildEmojiPickerView extends StatefulWidget {
|
||||||
const BuildEmojiPickerView({Key? key, required this.controller, this.offset})
|
const BuildEmojiPickerView({Key? key, this.offset}) : super(key: key);
|
||||||
: super(key: key);
|
|
||||||
|
|
||||||
final QuillController controller;
|
|
||||||
final Offset? offset;
|
final Offset? offset;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -172,15 +131,15 @@ class _BuildEmojiPickerViewState extends State<BuildEmojiPickerView> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void insertEmoji(Emoji emoji) {
|
void insertEmoji(Emoji emoji) {
|
||||||
final baseOffset = widget.controller.selection.baseOffset;
|
// final baseOffset = widget.controller.selection.baseOffset;
|
||||||
final extentOffset = widget.controller.selection.extentOffset;
|
// final extentOffset = widget.controller.selection.extentOffset;
|
||||||
final replaceLen = extentOffset - baseOffset;
|
// final replaceLen = extentOffset - baseOffset;
|
||||||
final selection = widget.controller.selection.copyWith(
|
// final selection = widget.controller.selection.copyWith(
|
||||||
baseOffset: baseOffset + emoji.emoji.length,
|
// baseOffset: baseOffset + emoji.emoji.length,
|
||||||
extentOffset: baseOffset + emoji.emoji.length,
|
// extentOffset: baseOffset + emoji.emoji.length,
|
||||||
);
|
// );
|
||||||
|
|
||||||
widget.controller
|
// widget.controller
|
||||||
.replaceText(baseOffset, replaceLen, emoji.emoji, selection);
|
// .replaceText(baseOffset, replaceLen, emoji.emoji, selection);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -246,13 +246,6 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.2.0"
|
version: "1.2.0"
|
||||||
cross_file:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: cross_file
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "0.3.3+1"
|
|
||||||
crypto:
|
crypto:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -466,34 +459,6 @@ packages:
|
|||||||
description: flutter
|
description: flutter
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.0"
|
version: "0.0.0"
|
||||||
flutter_inappwebview:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: flutter_inappwebview
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "5.4.3+7"
|
|
||||||
flutter_keyboard_visibility:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: flutter_keyboard_visibility
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "5.2.0"
|
|
||||||
flutter_keyboard_visibility_platform_interface:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: flutter_keyboard_visibility_platform_interface
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "2.0.0"
|
|
||||||
flutter_keyboard_visibility_web:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: flutter_keyboard_visibility_web
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "2.0.0"
|
|
||||||
flutter_lints:
|
flutter_lints:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
@ -513,15 +478,6 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.6"
|
version: "2.0.6"
|
||||||
flutter_quill:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
path: "."
|
|
||||||
ref: "306fd78b7a134abdde0fed6be67f59e8a6068509"
|
|
||||||
resolved-ref: "306fd78b7a134abdde0fed6be67f59e8a6068509"
|
|
||||||
url: "https://github.com/appflowy/flutter-quill.git"
|
|
||||||
source: git
|
|
||||||
version: "2.0.13"
|
|
||||||
flutter_svg:
|
flutter_svg:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -579,13 +535,6 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "7.2.0"
|
version: "7.2.0"
|
||||||
gettext_parser:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: gettext_parser
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "0.2.0"
|
|
||||||
glob:
|
glob:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -642,48 +591,6 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.0.1"
|
version: "4.0.1"
|
||||||
i18n_extension:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: i18n_extension
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "4.2.1"
|
|
||||||
image_picker:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: image_picker
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "0.8.5+3"
|
|
||||||
image_picker_android:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: image_picker_android
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "0.8.4+13"
|
|
||||||
image_picker_for_web:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: image_picker_for_web
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "2.1.8"
|
|
||||||
image_picker_ios:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: image_picker_ios
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "0.8.5+5"
|
|
||||||
image_picker_platform_interface:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: image_picker_platform_interface
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "2.5.0"
|
|
||||||
integration_test:
|
integration_test:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description: flutter
|
description: flutter
|
||||||
@ -955,13 +862,6 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.6"
|
version: "2.0.6"
|
||||||
pedantic:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: pedantic
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "1.11.1"
|
|
||||||
petitparser:
|
petitparser:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -969,13 +869,6 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "5.0.0"
|
version: "5.0.0"
|
||||||
photo_view:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: photo_view
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "0.13.0"
|
|
||||||
platform:
|
platform:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -1233,13 +1126,6 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.8.2"
|
version: "1.8.2"
|
||||||
sprintf:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: sprintf
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "6.0.0"
|
|
||||||
stack_trace:
|
stack_trace:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -1268,13 +1154,6 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.0"
|
version: "1.1.0"
|
||||||
string_validator:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: string_validator
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "0.3.0"
|
|
||||||
styled_widget:
|
styled_widget:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -1443,41 +1322,6 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.2"
|
version: "2.1.2"
|
||||||
video_player:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: video_player
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "2.4.2"
|
|
||||||
video_player_android:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: video_player_android
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "2.3.4"
|
|
||||||
video_player_avfoundation:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: video_player_avfoundation
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "2.3.4"
|
|
||||||
video_player_platform_interface:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: video_player_platform_interface
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "5.1.2"
|
|
||||||
video_player_web:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: video_player_web
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "2.0.10"
|
|
||||||
vm_service:
|
vm_service:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -1550,13 +1394,6 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.1"
|
version: "3.1.1"
|
||||||
youtube_player_flutter:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: youtube_player_flutter
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "8.1.0"
|
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=2.17.0 <3.0.0"
|
dart: ">=2.17.0 <3.0.0"
|
||||||
flutter: ">=3.0.0"
|
flutter: ">=3.0.0"
|
||||||
|
@ -43,10 +43,6 @@ dependencies:
|
|||||||
path: packages/appflowy_editor
|
path: packages/appflowy_editor
|
||||||
appflowy_popover:
|
appflowy_popover:
|
||||||
path: packages/appflowy_popover
|
path: packages/appflowy_popover
|
||||||
flutter_quill:
|
|
||||||
git:
|
|
||||||
url: https://github.com/appflowy/flutter-quill.git
|
|
||||||
ref: 306fd78b7a134abdde0fed6be67f59e8a6068509
|
|
||||||
|
|
||||||
# third party packages
|
# third party packages
|
||||||
intl: ^0.17.0
|
intl: ^0.17.0
|
||||||
|
Loading…
x
Reference in New Issue
Block a user