commit
e5d8dbd322
103 changed files with 3243 additions and 487 deletions
@ -0,0 +1 @@ |
||||
blank_issues_enabled: false |
File diff suppressed because one or more lines are too long
@ -1,10 +1,22 @@ |
||||
# Development notes |
||||
|
||||
- When updating the translations or localizations in the app, please take a look at the [Translation](./translation.md) page as it has important notes in order to work, if you also add a feature that adds new localizations then you need to the instructions of it in order for the translations to take effect |
||||
- We use the same package version and `CHANGELOG.md` for all the packages, for more [details](https://github.com/singerdmx/flutter-quill/pull/1878), the process is automated. We have a script that will do the followings: |
||||
1. Generate the `CHANGELOG.md` files by `CHANGELOG_JSON.json` (source of data) and then paste them into all the packages we have (overwrite), you don't need to |
||||
manually change/update any of the mentioned files above, once a new GitHub release published, the CI will take the release notes from the release, pass the info to the |
||||
script, the release notes can be auto-generated by Github using a button, a descirptive PRs title would help but you don't have to since we can change it at anytime. |
||||
2. The script require the new version as an argument, you don't need to run the script manually, when a maintainer create a new tag and publish a new GitHub release, the publish workflow will extract the new version from the tag name, run the script (pass the extracted version as an argument), commit the changes and push them into the repository, the script will update the `version` property for all the packages so the `flutter pub publish` will use the new version for each package correctly. |
||||
- When updating the translations or localizations in the app, please take a look at the [Translation](./translation.md) |
||||
page as it has important notes to work. |
||||
If you also add a feature that adds new localizations, then you need it |
||||
to the instructions of it in order for the translations to take effect |
||||
- We use the same package version and `CHANGELOG.md` for all the packages, for |
||||
more [details](https://github.com/singerdmx/flutter-quill/pull/1878), the process is automated. We have a script that |
||||
will do the following: |
||||
1. Generate the `CHANGELOG.md` files by `CHANGELOG_JSON.json` (source of data) and then paste them into all the |
||||
packages we have (overwrite), you don't need to |
||||
manually change/update any of the mentioned files above, once a new GitHub release published, the CI will take |
||||
the release notes from the release, pass the info to the |
||||
script, the release notes can be auto-generated by GitHub using a button, a descriptive PRs title would help but |
||||
you don't have to since we can change it at anytime. |
||||
2. The script require the new version as an argument, you don't need to run the script manually, when a maintainer |
||||
create a new tag and publish a new GitHub release, the publish workflow will extract the new version from the tag |
||||
name, run the script (pass the extracted version as an argument), commit the changes and push them into the |
||||
repository, the script will update the `version` property for all the packages so the `flutter pub publish` will |
||||
use the new version for each package correctly. |
||||
|
||||
the script will be used the CI and no need to run it manually |
||||
the script will be used the CI and no need to run it manually |
@ -1,55 +0,0 @@ |
||||
# Todo |
||||
|
||||
This is a todo list page that added recently and will be updated soon. |
||||
|
||||
## Table of contents |
||||
- [Todo](#todo) |
||||
- [Table of contents](#table-of-contents) |
||||
- [Flutter Quill](#flutter-quill) |
||||
- [Features](#features) |
||||
- [Improvemenets](#improvemenets) |
||||
- [Bugs](#bugs) |
||||
- [Flutter Quill Extensions](#flutter-quill-extensions) |
||||
- [Features](#features-1) |
||||
- [Improvemenets](#improvemenets-1) |
||||
- [Bugs](#bugs-1) |
||||
|
||||
## Flutter Quill |
||||
|
||||
### Features |
||||
|
||||
- Add support for Text magnification feature, for more [info](https://github.com/singerdmx/flutter-quill/issues/1504) |
||||
- Provide a way to expose quills undo redo stacks, for more [info](https://github.com/singerdmx/flutter-quill/issues/1381) |
||||
- Add callback to the `QuillToolbarColorButton` for custom color picking logic |
||||
|
||||
### Improvemenets |
||||
|
||||
- Improve the Raw Quill Editor, for more [info](https://github.com/singerdmx/flutter-quill/issues/1509) |
||||
- Provide more support to all the platforms |
||||
- Extract the shared properties between `QuillRawEditorConfigurations` and `QuillEditorConfigurations` |
||||
- The todo in the this [commit](https://github.com/singerdmx/flutter-quill/commit/79597ea6425357795c0663588ac079665241f23a) needs to be checked |
||||
- use `maybeOf` and of instead `ofNotNull` in the providers to follow flutter offical convenstion, completly rework the providers and update the build context extensions |
||||
- Add line through to the text when the check point checked is true |
||||
- Change the color of the numbers and dots in ol/ul to match the ones in the item list |
||||
- Fix the bugs of the font family and font size |
||||
- Try to update Quill Html Converter |
||||
- When pasting a HTML text from cliboard by not using the context menu builder, the new logic won't work |
||||
- When selecting all text and paste HTML text, it will not replace the current text, instead it will add a text |
||||
- Add strike-through in checkbox text when the checkpoint is checked |
||||
- No more using of dynamic |
||||
- There is a bug here, the first character is not being formatted when choosing font family or font size and type in the editor |
||||
- Fix the toolbar and the toolbar buttons, rework some of them, for example missing tooltips |
||||
|
||||
### Bugs |
||||
|
||||
Empty for now. |
||||
Please go to the [issues](https://github.com/singerdmx/flutter-quill/issues) |
||||
|
||||
|
||||
## Flutter Quill Extensions |
||||
|
||||
### Features |
||||
|
||||
### Improvemenets |
||||
|
||||
### Bugs |
@ -0,0 +1,75 @@ |
||||
import 'dart:async'; |
||||
import 'package:flutter/material.dart'; |
||||
|
||||
class TableCellWidget extends StatefulWidget { |
||||
const TableCellWidget({ |
||||
required this.cellId, |
||||
required this.cellData, |
||||
required this.onUpdate, |
||||
required this.onTap, |
||||
super.key, |
||||
}); |
||||
final String cellId; |
||||
final String cellData; |
||||
final Function(FocusNode node) onTap; |
||||
final Function(String data) onUpdate; |
||||
|
||||
@override |
||||
State<TableCellWidget> createState() => _TableCellWidgetState(); |
||||
} |
||||
|
||||
class _TableCellWidgetState extends State<TableCellWidget> { |
||||
late final TextEditingController controller; |
||||
late final FocusNode node; |
||||
Timer? _debounce; |
||||
@override |
||||
void initState() { |
||||
controller = TextEditingController(text: widget.cellData); |
||||
node = FocusNode(); |
||||
super.initState(); |
||||
} |
||||
|
||||
void _onTextChanged() { |
||||
if (!_debounce!.isActive) { |
||||
widget.onUpdate(controller.text); |
||||
return; |
||||
} |
||||
} |
||||
|
||||
@override |
||||
void dispose() { |
||||
controller |
||||
..removeListener(_onTextChanged) |
||||
..dispose(); |
||||
node.dispose(); |
||||
super.dispose(); |
||||
} |
||||
|
||||
@override |
||||
Widget build(BuildContext context) { |
||||
return Container( |
||||
width: 40, |
||||
constraints: const BoxConstraints( |
||||
minHeight: 50, |
||||
), |
||||
padding: const EdgeInsets.only(left: 5, right: 5, top: 5), |
||||
child: TextFormField( |
||||
controller: controller, |
||||
focusNode: node, |
||||
keyboardType: TextInputType.multiline, |
||||
maxLines: null, |
||||
decoration: const InputDecoration.collapsed(hintText: ''), |
||||
onTap: () { |
||||
widget.onTap.call(node); |
||||
}, |
||||
onTapAlwaysCalled: true, |
||||
onChanged: (value) { |
||||
_debounce = Timer( |
||||
const Duration(milliseconds: 900), |
||||
_onTextChanged, |
||||
); |
||||
}, |
||||
), |
||||
); |
||||
} |
||||
} |
@ -0,0 +1,234 @@ |
||||
import 'dart:convert'; |
||||
|
||||
import 'package:flutter/material.dart'; |
||||
import 'package:flutter_quill/flutter_quill.dart'; |
||||
import 'package:flutter_quill/quill_delta.dart'; |
||||
import '../../../utils/quill_table_utils.dart'; |
||||
import 'table_cell_embed.dart'; |
||||
import 'table_models.dart'; |
||||
|
||||
class CustomTableEmbed extends CustomBlockEmbed { |
||||
const CustomTableEmbed(String value) : super(tableType, value); |
||||
|
||||
static const String tableType = 'table'; |
||||
|
||||
static CustomTableEmbed fromDocument(Document document) => |
||||
CustomTableEmbed(jsonEncode(document.toDelta().toJson())); |
||||
|
||||
Document get document => Document.fromJson(jsonDecode(data)); |
||||
} |
||||
|
||||
//Embed builder |
||||
|
||||
class QuillEditorTableEmbedBuilder extends EmbedBuilder { |
||||
@override |
||||
String get key => 'table'; |
||||
|
||||
@override |
||||
Widget build( |
||||
BuildContext context, |
||||
QuillController controller, |
||||
Embed node, |
||||
bool readOnly, |
||||
bool inline, |
||||
TextStyle textStyle, |
||||
) { |
||||
final tableData = node.value.data; |
||||
return TableWidget( |
||||
tableData: tableData, |
||||
controller: controller, |
||||
); |
||||
} |
||||
} |
||||
|
||||
class TableWidget extends StatefulWidget { |
||||
const TableWidget({ |
||||
required this.tableData, |
||||
required this.controller, |
||||
super.key, |
||||
}); |
||||
final QuillController controller; |
||||
final Map<String, dynamic> tableData; |
||||
|
||||
@override |
||||
State<TableWidget> createState() => _TableWidgetState(); |
||||
} |
||||
|
||||
class _TableWidgetState extends State<TableWidget> { |
||||
TableModel _tableModel = TableModel(columns: {}, rows: {}); |
||||
String _selectedColumnId = ''; |
||||
String _selectedRowId = ''; |
||||
|
||||
@override |
||||
void initState() { |
||||
_tableModel = TableModel.fromMap(widget.tableData); |
||||
super.initState(); |
||||
} |
||||
|
||||
void _addColumn() { |
||||
setState(() { |
||||
final id = '${_tableModel.columns.length + 1}'; |
||||
final position = _tableModel.columns.length; |
||||
_tableModel.columns[id] = ColumnModel(id: id, position: position); |
||||
_tableModel.rows.forEach((key, row) { |
||||
row.cells[id] = ''; |
||||
}); |
||||
}); |
||||
_updateTable(); |
||||
} |
||||
|
||||
void _addRow() { |
||||
setState(() { |
||||
final id = '${_tableModel.rows.length + 1}'; |
||||
final cells = <String, String>{}; |
||||
_tableModel.columns.forEach((key, column) { |
||||
cells[key] = ''; |
||||
}); |
||||
_tableModel.rows[id] = RowModel(id: id, cells: cells); |
||||
}); |
||||
_updateTable(); |
||||
} |
||||
|
||||
void _removeColumn(String columnId) { |
||||
setState(() { |
||||
_tableModel.columns.remove(columnId); |
||||
_tableModel.rows.forEach((key, row) { |
||||
row.cells.remove(columnId); |
||||
}); |
||||
if (_selectedRowId == _selectedColumnId) { |
||||
_selectedRowId = ''; |
||||
} |
||||
_selectedColumnId = ''; |
||||
}); |
||||
_updateTable(); |
||||
} |
||||
|
||||
void _removeRow(String rowId) { |
||||
setState(() { |
||||
_tableModel.rows.remove(rowId); |
||||
_selectedRowId = ''; |
||||
}); |
||||
_updateTable(); |
||||
} |
||||
|
||||
void _updateCell(String columnId, String rowId, String data) { |
||||
setState(() { |
||||
_tableModel.rows[rowId]!.cells[columnId] = data; |
||||
}); |
||||
_updateTable(); |
||||
} |
||||
|
||||
void _updateTable() { |
||||
WidgetsBinding.instance.addPostFrameCallback((_) { |
||||
final offset = getEmbedNode( |
||||
widget.controller, |
||||
widget.controller.selection.start, |
||||
).offset; |
||||
final delta = Delta()..insert({'table': _tableModel.toMap()}); |
||||
widget.controller.replaceText( |
||||
offset, |
||||
1, |
||||
delta, |
||||
TextSelection.collapsed( |
||||
offset: offset, |
||||
), |
||||
); |
||||
}); |
||||
} |
||||
|
||||
@override |
||||
Widget build(BuildContext context) { |
||||
return Material( |
||||
child: Container( |
||||
decoration: BoxDecoration( |
||||
border: Border.all( |
||||
color: Theme.of(context).textTheme.bodyMedium?.color ?? |
||||
Colors.black)), |
||||
child: Column( |
||||
crossAxisAlignment: CrossAxisAlignment.end, |
||||
children: [ |
||||
IconButton( |
||||
icon: const Icon(Icons.more_vert), |
||||
onPressed: () async { |
||||
final position = renderPosition(context); |
||||
await showMenu<TableOperation>( |
||||
context: context, |
||||
position: position, |
||||
items: [ |
||||
const PopupMenuItem( |
||||
value: TableOperation.addColumn, |
||||
child: Text('Add column'), |
||||
), |
||||
const PopupMenuItem( |
||||
value: TableOperation.addRow, |
||||
child: Text('Add row'), |
||||
), |
||||
const PopupMenuItem( |
||||
value: TableOperation.removeColumn, |
||||
child: Text('Delete column'), |
||||
), |
||||
const PopupMenuItem( |
||||
value: TableOperation.removeRow, |
||||
child: Text('Delete row'), |
||||
), |
||||
]).then((value) { |
||||
if (value != null) { |
||||
if (value == TableOperation.addRow) { |
||||
_addRow(); |
||||
} |
||||
if (value == TableOperation.addColumn) { |
||||
_addColumn(); |
||||
} |
||||
if (value == TableOperation.removeColumn) { |
||||
_removeColumn(_selectedColumnId); |
||||
} |
||||
if (value == TableOperation.removeRow) { |
||||
_removeRow(_selectedRowId); |
||||
} |
||||
} |
||||
}); |
||||
}, |
||||
), |
||||
const Divider( |
||||
color: Colors.white, |
||||
height: 1, |
||||
), |
||||
Table( |
||||
border: const TableBorder.symmetric( |
||||
inside: BorderSide(color: Colors.white)), |
||||
children: _buildTableRows(), |
||||
), |
||||
], |
||||
), |
||||
), |
||||
); |
||||
} |
||||
|
||||
List<TableRow> _buildTableRows() { |
||||
final rows = <TableRow>[]; |
||||
|
||||
_tableModel.rows.forEach((rowId, rowModel) { |
||||
final rowCells = <Widget>[]; |
||||
final rowKey = rowId; |
||||
rowModel.cells.forEach((key, value) { |
||||
if (key != 'id') { |
||||
final columnId = key; |
||||
final data = value; |
||||
rowCells.add(TableCellWidget( |
||||
cellId: rowKey, |
||||
onTap: (node) { |
||||
setState(() { |
||||
_selectedColumnId = columnId; |
||||
_selectedRowId = rowModel.id; |
||||
}); |
||||
}, |
||||
cellData: data, |
||||
onUpdate: (data) => _updateCell(columnId, rowKey, data), |
||||
)); |
||||
} |
||||
}); |
||||
rows.add(TableRow(children: rowCells)); |
||||
}); |
||||
return rows; |
||||
} |
||||
} |
@ -0,0 +1,85 @@ |
||||
class TableModel { |
||||
TableModel({required this.columns, required this.rows}); |
||||
|
||||
factory TableModel.fromMap(Map<String, dynamic> json) { |
||||
return TableModel( |
||||
columns: (json['columns'] as Map<String, dynamic>).map( |
||||
(key, value) => MapEntry( |
||||
key, |
||||
ColumnModel.fromMap( |
||||
value, |
||||
), |
||||
), |
||||
), |
||||
rows: (json['rows'] as Map<String, dynamic>).map( |
||||
(key, value) => MapEntry( |
||||
key, |
||||
RowModel.fromMap( |
||||
value, |
||||
), |
||||
), |
||||
), |
||||
); |
||||
} |
||||
Map<String, ColumnModel> columns; |
||||
Map<String, RowModel> rows; |
||||
|
||||
Map<String, dynamic> toMap() { |
||||
return { |
||||
'columns': columns.map( |
||||
(key, value) => MapEntry( |
||||
key, |
||||
value.toMap(), |
||||
), |
||||
), |
||||
'rows': rows.map( |
||||
(key, value) => MapEntry( |
||||
key, |
||||
value.toMap(), |
||||
), |
||||
), |
||||
}; |
||||
} |
||||
} |
||||
|
||||
class ColumnModel { |
||||
ColumnModel({required this.id, required this.position}); |
||||
|
||||
factory ColumnModel.fromMap(Map<String, dynamic> json) { |
||||
return ColumnModel( |
||||
id: json['id'], |
||||
position: json['position'], |
||||
); |
||||
} |
||||
String id; |
||||
int position; |
||||
|
||||
Map<String, dynamic> toMap() { |
||||
return { |
||||
'id': id, |
||||
'position': position, |
||||
}; |
||||
} |
||||
} |
||||
|
||||
class RowModel { |
||||
// Key is column ID, value is cell content |
||||
|
||||
RowModel({required this.id, required this.cells}); |
||||
|
||||
factory RowModel.fromMap(Map<String, dynamic> json) { |
||||
return RowModel( |
||||
id: json['id'], |
||||
cells: Map<String, String>.from(json['cells']), |
||||
); |
||||
} |
||||
String id; |
||||
Map<String, String> cells; |
||||
|
||||
Map<String, dynamic> toMap() { |
||||
return { |
||||
'id': id, |
||||
'cells': cells, |
||||
}; |
||||
} |
||||
} |
@ -0,0 +1,113 @@ |
||||
import 'package:flutter/material.dart'; |
||||
import 'package:flutter_quill/flutter_quill.dart'; |
||||
import 'package:flutter_quill/translations.dart'; |
||||
import '../../../models/config/table/table_configurations.dart'; |
||||
import '../../../utils/quill_table_utils.dart'; |
||||
|
||||
class QuillToolbarTableButton extends StatelessWidget { |
||||
const QuillToolbarTableButton({ |
||||
required this.controller, |
||||
this.options = const QuillToolbarTableButtonOptions(), |
||||
super.key, |
||||
}); |
||||
|
||||
final QuillController controller; |
||||
|
||||
final QuillToolbarTableButtonOptions options; |
||||
|
||||
double _iconSize(BuildContext context) { |
||||
final baseFontSize = baseButtonExtraOptions(context)?.iconSize; |
||||
final iconSize = options.iconSize; |
||||
return iconSize ?? baseFontSize ?? kDefaultIconSize; |
||||
} |
||||
|
||||
double _iconButtonFactor(BuildContext context) { |
||||
final baseIconFactor = baseButtonExtraOptions(context)?.iconButtonFactor; |
||||
final iconButtonFactor = options.iconButtonFactor; |
||||
return iconButtonFactor ?? baseIconFactor ?? kDefaultIconButtonFactor; |
||||
} |
||||
|
||||
VoidCallback? _afterButtonPressed(BuildContext context) { |
||||
return options.afterButtonPressed ?? |
||||
baseButtonExtraOptions(context)?.afterButtonPressed; |
||||
} |
||||
|
||||
QuillIconTheme? _iconTheme(BuildContext context) { |
||||
return options.iconTheme ?? baseButtonExtraOptions(context)?.iconTheme; |
||||
} |
||||
|
||||
QuillToolbarBaseButtonOptions? baseButtonExtraOptions(BuildContext context) { |
||||
return context.quillToolbarBaseButtonOptions; |
||||
} |
||||
|
||||
IconData _iconData(BuildContext context) { |
||||
return options.iconData ?? |
||||
baseButtonExtraOptions(context)?.iconData ?? |
||||
Icons.table_chart; |
||||
} |
||||
|
||||
String _tooltip(BuildContext context) { |
||||
return options.tooltip ?? |
||||
baseButtonExtraOptions(context)?.tooltip ?? |
||||
context.loc.insertTable; |
||||
} |
||||
|
||||
void _sharedOnPressed(BuildContext context) { |
||||
_onPressedHandler(context); |
||||
_afterButtonPressed(context); |
||||
} |
||||
|
||||
@override |
||||
Widget build(BuildContext context) { |
||||
final tooltip = _tooltip(context); |
||||
final iconSize = _iconSize(context); |
||||
final iconButtonFactor = _iconButtonFactor(context); |
||||
final iconData = _iconData(context); |
||||
final childBuilder = |
||||
options.childBuilder ?? baseButtonExtraOptions(context)?.childBuilder; |
||||
|
||||
if (childBuilder != null) { |
||||
return childBuilder( |
||||
QuillToolbarTableButtonOptions( |
||||
afterButtonPressed: _afterButtonPressed(context), |
||||
iconData: iconData, |
||||
iconSize: iconSize, |
||||
iconButtonFactor: iconButtonFactor, |
||||
iconTheme: options.iconTheme, |
||||
tooltip: options.tooltip, |
||||
), |
||||
QuillToolbarTableButtonExtraOptions( |
||||
context: context, |
||||
controller: controller, |
||||
onPressed: () => _sharedOnPressed(context), |
||||
), |
||||
); |
||||
} |
||||
|
||||
return QuillToolbarIconButton( |
||||
icon: Icon( |
||||
iconData, |
||||
size: iconButtonFactor * iconSize, |
||||
), |
||||
tooltip: tooltip, |
||||
isSelected: false, |
||||
onPressed: () => _sharedOnPressed(context), |
||||
iconTheme: _iconTheme(context), |
||||
); |
||||
} |
||||
|
||||
Future<void> _onPressedHandler(BuildContext context) async { |
||||
final position = renderPosition(context); |
||||
await showMenu(context: context, position: position, items: [ |
||||
const PopupMenuItem(value: 2, child: Text('2x2')), |
||||
const PopupMenuItem(value: 4, child: Text('4x4')), |
||||
const PopupMenuItem(value: 6, child: Text('6x6')), |
||||
]).then( |
||||
(value) { |
||||
if (value != null) { |
||||
insertTable(value, value, controller, ChangeSource.local); |
||||
} |
||||
}, |
||||
); |
||||
} |
||||
} |
@ -0,0 +1,27 @@ |
||||
import 'package:flutter_quill/flutter_quill.dart'; |
||||
import 'package:meta/meta.dart' show immutable; |
||||
|
||||
class QuillToolbarTableButtonExtraOptions |
||||
extends QuillToolbarBaseButtonExtraOptions { |
||||
const QuillToolbarTableButtonExtraOptions({ |
||||
required super.controller, |
||||
required super.context, |
||||
required super.onPressed, |
||||
}); |
||||
} |
||||
|
||||
@immutable |
||||
class QuillToolbarTableButtonOptions extends QuillToolbarBaseButtonOptions< |
||||
QuillToolbarTableButtonOptions, QuillToolbarTableButtonExtraOptions> { |
||||
const QuillToolbarTableButtonOptions({ |
||||
super.iconData, |
||||
super.iconSize, |
||||
super.iconButtonFactor, |
||||
|
||||
/// specifies the tooltip text for the image button. |
||||
super.tooltip, |
||||
super.afterButtonPressed, |
||||
super.childBuilder, |
||||
super.iconTheme, |
||||
}); |
||||
} |
@ -0,0 +1,85 @@ |
||||
import 'package:flutter/widgets.dart' |
||||
show |
||||
BuildContext, |
||||
MediaQuery, |
||||
Offset, |
||||
Overlay, |
||||
Rect, |
||||
RelativeRect, |
||||
RenderBox, |
||||
Size, |
||||
TextSelection; |
||||
import 'package:flutter_quill/flutter_quill.dart'; |
||||
import 'package:flutter_quill/quill_delta.dart'; |
||||
|
||||
enum TableOperation { |
||||
addColumn, |
||||
addRow, |
||||
removeColumn, |
||||
removeRow, |
||||
} |
||||
|
||||
RelativeRect renderPosition(BuildContext context, [Size? size]) { |
||||
size ??= MediaQuery.sizeOf(context); |
||||
final overlay = Overlay.of(context).context.findRenderObject() as RenderBox; |
||||
final button = context.findRenderObject() as RenderBox; |
||||
final position = RelativeRect.fromRect( |
||||
Rect.fromPoints( |
||||
button.localToGlobal(const Offset(0, -65), ancestor: overlay), |
||||
button.localToGlobal( |
||||
button.size.bottomRight(Offset.zero) + const Offset(-50, 0), |
||||
ancestor: overlay), |
||||
), |
||||
Offset.zero & size * 0.40, |
||||
); |
||||
return position; |
||||
} |
||||
|
||||
void insertTable(int rows, int columns, QuillController quillController, |
||||
ChangeSource? changeFrom) { |
||||
final tableData = _createTableData(rows, columns); |
||||
final delta = Delta()..insert({'table': tableData}); |
||||
final selection = quillController.selection; |
||||
final replacedLength = selection.extentOffset - selection.baseOffset; |
||||
final newBaseOffset = selection.baseOffset; |
||||
final newExtentOffsetCandidate = |
||||
(selection.baseOffset + 1 - replacedLength).toInt(); |
||||
final newExtentOffsetAdjusted = |
||||
newExtentOffsetCandidate < 0 ? 0 : newExtentOffsetCandidate; |
||||
quillController.replaceText( |
||||
newBaseOffset, |
||||
replacedLength, |
||||
delta, |
||||
TextSelection( |
||||
baseOffset: newBaseOffset, extentOffset: newExtentOffsetAdjusted), |
||||
); |
||||
} |
||||
|
||||
Map<String, dynamic> _createTableData(int rows, int columns) { |
||||
// Crear el mapa para las columnas |
||||
final columnsData = <String, dynamic>{}; |
||||
for (var col = 0; col < columns; col++) { |
||||
final columnId = '${col + 1}'; |
||||
columnsData[columnId] = {'id': columnId, 'position': col}; |
||||
} |
||||
|
||||
// Crear el mapa para las filas |
||||
final rowsData = <String, dynamic>{}; |
||||
for (var row = 0; row < rows; row++) { |
||||
final rowId = '${row + 1}'; |
||||
rowsData[rowId] = {'id': rowId, 'cells': {}}; |
||||
|
||||
for (var col = 0; col < columns; col++) { |
||||
final columnId = '${col + 1}'; |
||||
rowsData[rowId]['cells'][columnId] = ''; |
||||
} |
||||
} |
||||
|
||||
// Combinar las columnas y filas en una estructura de tabla |
||||
final tableData = <String, dynamic>{ |
||||
'columns': columnsData, |
||||
'rows': rowsData, |
||||
}; |
||||
|
||||
return tableData; |
||||
} |
@ -1,8 +1,14 @@ |
||||
class QuillControllerConfigurations { |
||||
const QuillControllerConfigurations({this.onClipboardPaste}); |
||||
const QuillControllerConfigurations( |
||||
{this.onClipboardPaste, this.requireScriptFontFeatures = false}); |
||||
|
||||
/// Callback when the user pastes and data has not already been processed |
||||
/// |
||||
/// Return true if the paste operation was handled |
||||
final Future<bool> Function()? onClipboardPaste; |
||||
|
||||
/// Render subscript and superscript text using Open Type FontFeatures |
||||
/// |
||||
/// Default is false to use built-in script rendering that is independent of font capabilities |
||||
final bool requireScriptFontFeatures; |
||||
} |
||||
|
@ -0,0 +1,80 @@ |
||||
import 'package:html2md/html2md.dart' as hmd; |
||||
import 'package:markdown/markdown.dart' as md; |
||||
|
||||
// [ character |
||||
const int _$lbracket = 0x5B; |
||||
final RegExp _youtubeVideoUrlValidator = RegExp( |
||||
r'^((?:https?:)?\/\/)?((?:www|m)\.)?((?:youtube\.com|youtu.be))(\/(?:[\w\-]+\?v=|embed\/|v\/)?)([\w\-]+)(\S+)?$'); |
||||
|
||||
///Local syntax implementation for underline |
||||
class UnderlineSyntax extends md.DelimiterSyntax { |
||||
UnderlineSyntax() |
||||
: super( |
||||
'<und>', |
||||
requiresDelimiterRun: true, |
||||
allowIntraWord: true, |
||||
tags: [md.DelimiterTag('u', 5)], |
||||
); |
||||
} |
||||
|
||||
class VideoSyntax extends md.LinkSyntax { |
||||
VideoSyntax({super.linkResolver}) |
||||
: super( |
||||
pattern: r'\[', |
||||
startCharacter: _$lbracket, |
||||
); |
||||
|
||||
@override |
||||
md.Element createNode( |
||||
String destination, |
||||
String? title, { |
||||
required List<md.Node> Function() getChildren, |
||||
}) { |
||||
final element = md.Element.empty('video'); |
||||
element.attributes['src'] = destination; |
||||
if (title != null && title.isNotEmpty) { |
||||
element.attributes['title'] = title; |
||||
} |
||||
return element; |
||||
} |
||||
} |
||||
|
||||
///This rule avoid the default converter from html2md ignore underline tag for <u> or <ins> |
||||
final underlineRule = |
||||
hmd.Rule('underline', filters: ['u', 'ins'], replacement: (content, node) { |
||||
//Is used a local underline implemenation since markdown just use underline with html tags |
||||
return '<und>$content<und>'; |
||||
}); |
||||
final videoRule = hmd.Rule('video', filters: ['iframe', 'video'], |
||||
replacement: (content, node) { |
||||
//This need to be verified by a different way of iframes, since video tag can have <source> children |
||||
if (node.nodeName == 'video') { |
||||
//if has children then just will be taked as different part of code |
||||
if (node.childNum > 0) { |
||||
var child = node.firstChild!; |
||||
var src = child.getAttribute('src'); |
||||
if (src == null) { |
||||
child = node.childNodes().last; |
||||
src = child.getAttribute('src'); |
||||
} |
||||
if (!_youtubeVideoUrlValidator.hasMatch(src ?? '')) { |
||||
return '<video>${child.outerHTML}</video>'; |
||||
} |
||||
return '[$content]($src)'; |
||||
} |
||||
final src = node.getAttribute('src'); |
||||
if (src == null || !_youtubeVideoUrlValidator.hasMatch(src)) { |
||||
return node.outerHTML; |
||||
} |
||||
return '[$content]($src)'; |
||||
} |
||||
//by now, we can only access to src |
||||
final src = node.getAttribute('src'); |
||||
//if the source is null or is not valid youtube url, then just return the html instead remove it |
||||
//by now is only available validation for youtube videos |
||||
if (src == null || !_youtubeVideoUrlValidator.hasMatch(src)) { |
||||
return node.outerHTML; |
||||
} |
||||
final title = node.getAttribute('title'); |
||||
return '[$title]($src)'; |
||||
}); |
@ -0,0 +1,24 @@ |
||||
// ignore_for_file: avoid_print |
||||
|
||||
import 'dart:io' show Directory, Process; |
||||
|
||||
Future<void> main(List<String> args) async { |
||||
final generatedDartLocalizationsFolder = Directory('lib/src/l10n/generated'); |
||||
if (await generatedDartLocalizationsFolder.exists()) { |
||||
print( |
||||
'Generated directory (${generatedDartLocalizationsFolder.path}) exists, deleting it... 📁', |
||||
); |
||||
await generatedDartLocalizationsFolder.delete(recursive: true); |
||||
} |
||||
print('Running flutter pub get... 📦'); |
||||
await Process.run('flutter', ['pub', 'get']); |
||||
|
||||
print('Running flutter gen-l10n... 🌍'); |
||||
await Process.run('flutter', ['gen-l10n']); |
||||
|
||||
print('Applying Dart fixes to the newly generated files... 🔧'); |
||||
await Process.run('dart', ['fix', '--apply', './lib/src/l10n/generated']); |
||||
|
||||
print('Formatting the newly generated Dart files... ✨'); |
||||
await Process.run('dart', ['format', './lib/src/l10n/generated']); |
||||
} |
@ -1,29 +0,0 @@ |
||||
#!/bin/bash |
||||
|
||||
# Important: make sure to run the script in the root folder of the repo: |
||||
# ./scripts/regenerate_translations.sh |
||||
# otherwise the script could delete the wrong folder in rare cases |
||||
|
||||
# TODO: Refactor this to a dart script to allow developers who use Windows to use it |
||||
|
||||
echo "" |
||||
|
||||
echo "Delete the current generated localizations..." |
||||
rm -rf lib/src/l10n/generated |
||||
echo "" |
||||
|
||||
echo "Run flutter pub get.." |
||||
flutter pub get |
||||
echo "" |
||||
|
||||
echo "Run flutter gen-l10n" |
||||
flutter gen-l10n |
||||
echo "" |
||||
|
||||
echo "" |
||||
echo "Apply dart fixes to the newly generated files" |
||||
dart fix --apply ./lib/src/l10n/generated |
||||
|
||||
echo "" |
||||
echo "Formate the newly generated dart files" |
||||
dart format ./lib/src/l10n/generated |
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue