feat: integrate Flutter Quill for rich text editing and update note handling
- Added Flutter Quill package for rich text editing capabilities. - Refactored note body handling to support Quill's Document format. - Updated note editor screen to use QuillEditor and QuillController. - Enhanced note search functionality to convert note body to plain text. - Modified note card display to show plain text from note body. - Updated localization support for Quill in the app. - Registered necessary plugins for URL launching and file selection on all platforms. - Updated app icons and other assets for consistency across platforms. - Updated pubspec.yaml and pubspec.lock to include new dependencies and versions.
This commit is contained in:
@@ -3,6 +3,8 @@ import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'package:flutter_quill/flutter_quill.dart';
|
||||
import 'package:notas/data/app_database.dart';
|
||||
import 'package:notas/data/api_client.dart';
|
||||
import 'package:notas/data/local_vault_service.dart';
|
||||
@@ -1094,6 +1096,12 @@ class _NotesAppState extends State<NotesApp>
|
||||
title: 'Mis Notas',
|
||||
debugShowCheckedModeBanner: false,
|
||||
scaffoldMessengerKey: _scaffoldMessengerKey,
|
||||
localizationsDelegates: const <LocalizationsDelegate<dynamic>>[
|
||||
GlobalMaterialLocalizations.delegate,
|
||||
GlobalWidgetsLocalizations.delegate,
|
||||
GlobalCupertinoLocalizations.delegate,
|
||||
FlutterQuillLocalizations.delegate,
|
||||
],
|
||||
theme:
|
||||
_lightTheme ??
|
||||
AppTheme.theme(
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter_quill/flutter_quill.dart';
|
||||
|
||||
Document noteBodyToDocument(String storedBody) {
|
||||
final String trimmedBody = storedBody.trimLeft();
|
||||
|
||||
if (trimmedBody.startsWith('[')) {
|
||||
try {
|
||||
final dynamic decoded = jsonDecode(storedBody);
|
||||
if (decoded is List) {
|
||||
return Document.fromJson(decoded);
|
||||
}
|
||||
} catch (_) {
|
||||
// Fall back to plain text for legacy notes or malformed JSON.
|
||||
}
|
||||
}
|
||||
|
||||
if (storedBody.isEmpty) {
|
||||
return Document();
|
||||
}
|
||||
|
||||
final String plainText = storedBody.endsWith('\n')
|
||||
? storedBody
|
||||
: '$storedBody\n';
|
||||
|
||||
return Document.fromJson(<dynamic>[
|
||||
<String, String>{'insert': plainText},
|
||||
]);
|
||||
}
|
||||
|
||||
String noteBodyToPlainText(String storedBody) {
|
||||
return noteBodyToDocument(storedBody).toPlainText();
|
||||
}
|
||||
|
||||
String noteDocumentToStorageJson(Document document) {
|
||||
return jsonEncode(document.toDelta().toJson());
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
|
||||
|
||||
import 'package:notas/data/note_body.dart';
|
||||
import 'package:notas/data/note_repository.dart';
|
||||
import 'package:notas/models/note.dart';
|
||||
import 'package:notas/screens/note_editor_screen.dart';
|
||||
@@ -290,7 +291,7 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||
.where(
|
||||
(Note note) =>
|
||||
note.title.toLowerCase().contains(query) ||
|
||||
note.body.toLowerCase().contains(query),
|
||||
noteBodyToPlainText(note.body).toLowerCase().contains(query),
|
||||
)
|
||||
.toList();
|
||||
}
|
||||
@@ -713,7 +714,7 @@ class _DraggableNote extends StatelessWidget {
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
note.body,
|
||||
noteBodyToPlainText(note.body),
|
||||
style: TextStyle(color: palette.textSecondary, fontSize: 14),
|
||||
maxLines: 20,
|
||||
overflow: TextOverflow.clip,
|
||||
|
||||
@@ -2,8 +2,10 @@ import 'dart:async';
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_quill/flutter_quill.dart' hide Text;
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
import 'package:notas/data/note_body.dart';
|
||||
import 'package:notas/models/category.dart';
|
||||
import 'package:notas/models/note.dart';
|
||||
import 'package:notas/platform/app_platform.dart';
|
||||
@@ -110,7 +112,9 @@ class NoteEditorScreen extends StatefulWidget {
|
||||
|
||||
class _NoteEditorScreenState extends State<NoteEditorScreen> {
|
||||
late TextEditingController _titleController;
|
||||
late TextEditingController _bodyController;
|
||||
late QuillController _bodyController;
|
||||
late FocusNode _bodyFocusNode;
|
||||
late ScrollController _bodyScrollController;
|
||||
late Note _currentNote;
|
||||
late bool _isNewNote;
|
||||
String? _selectedCategoryId;
|
||||
@@ -147,13 +151,20 @@ class _NoteEditorScreenState extends State<NoteEditorScreen> {
|
||||
_selectedCategoryId = _currentNote.categoryId ?? widget.categoryId;
|
||||
|
||||
_titleController = TextEditingController(text: _currentNote.title);
|
||||
_bodyController = TextEditingController(text: _currentNote.body);
|
||||
_bodyController = QuillController(
|
||||
document: noteBodyToDocument(_currentNote.body),
|
||||
selection: const TextSelection.collapsed(offset: 0),
|
||||
);
|
||||
_bodyFocusNode = FocusNode();
|
||||
_bodyScrollController = ScrollController();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_closeCategoryMenu();
|
||||
_titleController.dispose();
|
||||
_bodyFocusNode.dispose();
|
||||
_bodyScrollController.dispose();
|
||||
_bodyController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
@@ -182,17 +193,17 @@ class _NoteEditorScreenState extends State<NoteEditorScreen> {
|
||||
|
||||
void _saveNote() {
|
||||
final String title = _titleController.text.trim();
|
||||
final String body = _bodyController.text.trim();
|
||||
final String bodyPlainText = _bodyController.document.toPlainText().trim();
|
||||
final bool categoryChanged = _selectedCategoryId != _currentNote.categoryId;
|
||||
|
||||
if (title.isEmpty && body.isEmpty && !categoryChanged) {
|
||||
if (title.isEmpty && bodyPlainText.isEmpty && !categoryChanged) {
|
||||
_complete(null);
|
||||
return;
|
||||
}
|
||||
|
||||
final Note updatedNote = _currentNote.copyWith(
|
||||
title: title.isEmpty ? 'Sin título' : title,
|
||||
body: body,
|
||||
body: noteDocumentToStorageJson(_bodyController.document),
|
||||
categoryId: _selectedCategoryId,
|
||||
updatedAt: DateTime.now(),
|
||||
isDirty: true,
|
||||
@@ -646,21 +657,20 @@ class _NoteEditorScreenState extends State<NoteEditorScreen> {
|
||||
),
|
||||
SizedBox(height: titleSpacing),
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: _bodyController,
|
||||
keyboardType: TextInputType.multiline,
|
||||
maxLines: null,
|
||||
expands: true,
|
||||
style: TextStyle(
|
||||
color: palette.textPrimary,
|
||||
fontSize: 16,
|
||||
height: 1.6,
|
||||
),
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Escribe tu nota...',
|
||||
hintStyle: TextStyle(color: palette.textHint),
|
||||
border: InputBorder.none,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(top: 4),
|
||||
child: QuillEditor.basic(
|
||||
controller: _bodyController,
|
||||
focusNode: _bodyFocusNode,
|
||||
scrollController: _bodyScrollController,
|
||||
config: QuillEditorConfig(
|
||||
scrollable: true,
|
||||
padding: EdgeInsets.zero,
|
||||
autoFocus: false,
|
||||
expands: true,
|
||||
placeholder: 'Escribe tu nota...',
|
||||
keyboardAppearance: Theme.of(context).brightness,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -673,21 +683,67 @@ class _NoteEditorScreenState extends State<NoteEditorScreen> {
|
||||
decoration: BoxDecoration(
|
||||
border: Border(top: BorderSide(color: palette.border, width: 1)),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (!_isNewNote)
|
||||
IconButton(
|
||||
onPressed: _deleteNote,
|
||||
icon: Icon(
|
||||
Icons.delete_outline,
|
||||
color: palette.destructiveAccent,
|
||||
QuillSimpleToolbar(
|
||||
controller: _bodyController,
|
||||
config: const QuillSimpleToolbarConfig(
|
||||
color: Colors.transparent,
|
||||
showBoldButton: true,
|
||||
showItalicButton: true,
|
||||
showUnderLineButton: true,
|
||||
showStrikeThrough: false,
|
||||
showInlineCode: false,
|
||||
showColorButton: false,
|
||||
showBackgroundColorButton: false,
|
||||
showClearFormat: false,
|
||||
showAlignmentButtons: false,
|
||||
showHeaderStyle: false,
|
||||
showListNumbers: true,
|
||||
showListBullets: true,
|
||||
showListCheck: true,
|
||||
showCodeBlock: false,
|
||||
showQuote: false,
|
||||
showIndent: false,
|
||||
showLink: false,
|
||||
showUndo: false,
|
||||
showRedo: false,
|
||||
showDividers: false,
|
||||
showFontFamily: false,
|
||||
showFontSize: false,
|
||||
showDirection: false,
|
||||
showSearchButton: false,
|
||||
showSubscript: false,
|
||||
showSuperscript: false,
|
||||
multiRowsDisplay: false,
|
||||
showClipboardCut: false,
|
||||
showClipboardCopy: false,
|
||||
showClipboardPaste: false,
|
||||
axis: Axis.horizontal,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
if (!_isNewNote)
|
||||
IconButton(
|
||||
onPressed: _deleteNote,
|
||||
icon: Icon(
|
||||
Icons.delete_outline,
|
||||
color: palette.destructiveAccent,
|
||||
),
|
||||
tooltip: 'Eliminar nota',
|
||||
)
|
||||
else
|
||||
const SizedBox(width: 48),
|
||||
FilledButton(
|
||||
onPressed: _saveNote,
|
||||
child: const Text('Guardar'),
|
||||
),
|
||||
tooltip: 'Eliminar nota',
|
||||
)
|
||||
else
|
||||
const SizedBox(width: 48),
|
||||
FilledButton(onPressed: _saveNote, child: const Text('Guardar')),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:notas/data/note_body.dart';
|
||||
import 'package:notas/models/note.dart';
|
||||
import 'package:notas/theme/app_palette.dart';
|
||||
|
||||
@@ -77,26 +78,27 @@ class _NoteCardState extends State<NoteCard> {
|
||||
// running the expensive TextPainter layout. This heuristic counts
|
||||
// newline characters and estimates wrapped lines based on an
|
||||
// average characters-per-line to handle many short lines well.
|
||||
final List<String> rawLines = widget.note.body.split('\n');
|
||||
final String bodyText = noteBodyToPlainText(widget.note.body);
|
||||
final List<String> rawLines = bodyText.split('\n');
|
||||
const int avgCharsPerLine = 40;
|
||||
int estimatedLines = 0;
|
||||
for (final String line in rawLines) {
|
||||
estimatedLines += (line.trim().length ~/ avgCharsPerLine) + 1;
|
||||
}
|
||||
|
||||
final bool needsPreciseMeasurement = estimatedLines > 20;
|
||||
final bool needsPreciseMeasurement = estimatedLines > 15;
|
||||
final bool isBodyTruncated;
|
||||
|
||||
if (needsPreciseMeasurement) {
|
||||
final TextPainter textPainter = TextPainter(
|
||||
text: TextSpan(
|
||||
text: widget.note.body,
|
||||
text: bodyText,
|
||||
style: TextStyle(
|
||||
color: palette.textSecondary,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
maxLines: 20,
|
||||
maxLines: 15,
|
||||
textDirection: TextDirection.ltr,
|
||||
)..layout(maxWidth: constraints.maxWidth);
|
||||
|
||||
@@ -121,12 +123,12 @@ class _NoteCardState extends State<NoteCard> {
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
widget.note.body,
|
||||
bodyText,
|
||||
style: TextStyle(
|
||||
color: palette.textSecondary,
|
||||
fontSize: 14,
|
||||
),
|
||||
maxLines: 20,
|
||||
maxLines: 15,
|
||||
overflow: TextOverflow.clip,
|
||||
),
|
||||
if (isBodyTruncated) ...[
|
||||
|
||||
Reference in New Issue
Block a user