feat: add built-in markdown editor with search jump and typing helpers
This commit is contained in:
parent
db27f64e42
commit
ed2de5e67f
16 changed files with 1146 additions and 561 deletions
|
|
@ -1,33 +1,38 @@
|
|||
import 'dart:io';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_code_editor/flutter_code_editor.dart';
|
||||
import 'package:flutter_markdown/flutter_markdown.dart';
|
||||
import 'package:flutter_markdown_latex/flutter_markdown_latex.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
import 'package:markdown/markdown.dart' as md;
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:share_plus/share_plus.dart';
|
||||
import 'themes/day.dart';
|
||||
import 'themes/night.dart';
|
||||
import 'themes/markdown.dart';
|
||||
import 'codefield.dart';
|
||||
import 'markdown_editing.dart';
|
||||
import 'markdown_view.dart';
|
||||
|
||||
class NoteEditorScreen extends StatefulWidget {
|
||||
final File note;
|
||||
final String? notesDirectoryPath; // ✅ Add this
|
||||
final bool isDarkMode;
|
||||
final List<String> searchTerms;
|
||||
|
||||
NoteEditorScreen(
|
||||
{required this.note,
|
||||
required this.isDarkMode,
|
||||
required this.notesDirectoryPath});
|
||||
required this.notesDirectoryPath,
|
||||
this.searchTerms = const []});
|
||||
|
||||
@override
|
||||
_NoteEditorScreenState createState() => _NoteEditorScreenState();
|
||||
}
|
||||
|
||||
class _NoteEditorScreenState extends State<NoteEditorScreen> {
|
||||
late CodeController _controller;
|
||||
late MarkdownController _controller;
|
||||
late String _initialContent;
|
||||
final _viewKey = GlobalKey<MarkdownViewState>();
|
||||
final _editorFocus = FocusNode();
|
||||
bool _isEditing = false;
|
||||
bool _isPreviewMode = false;
|
||||
bool _isLoading = true;
|
||||
bool _autoSaveEnabled = false; // Controlled by user toggle
|
||||
|
|
@ -64,13 +69,35 @@ class _NoteEditorScreenState extends State<NoteEditorScreen> {
|
|||
}
|
||||
|
||||
setState(() {
|
||||
_controller = CodeController(
|
||||
_controller = MarkdownController(
|
||||
text: _initialContent,
|
||||
language: markdown,
|
||||
);
|
||||
theme: widget.isDarkMode ? nightTheme : dayTheme,
|
||||
)..search(widget.searchTerms);
|
||||
_controller.addListener(_onTextChanged);
|
||||
_isLoading = false;
|
||||
});
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => _showMatch(0));
|
||||
}
|
||||
|
||||
void _showMatch(int index) {
|
||||
final matches = _controller.matches;
|
||||
if (matches.isEmpty) return;
|
||||
final current = index % matches.length;
|
||||
_controller.selectMatch(current);
|
||||
_viewKey.currentState?.revealOffset(matches[current].start);
|
||||
}
|
||||
|
||||
void _toggleEditing() {
|
||||
setState(() {
|
||||
_isEditing = !_isEditing;
|
||||
_isPreviewMode = false;
|
||||
});
|
||||
if (_isEditing) {
|
||||
WidgetsBinding.instance
|
||||
.addPostFrameCallback((_) => _editorFocus.requestFocus());
|
||||
} else {
|
||||
_editorFocus.unfocus();
|
||||
}
|
||||
}
|
||||
|
||||
void _onTextChanged() {
|
||||
|
|
@ -92,6 +119,12 @@ class _NoteEditorScreenState extends State<NoteEditorScreen> {
|
|||
}
|
||||
}
|
||||
|
||||
void _shareNote() {
|
||||
if (_controller.text.isNotEmpty) {
|
||||
Share.share(_controller.text, subject: widget.note.uri.pathSegments.last);
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> _onWillPop() async {
|
||||
if (_controller.text == _initialContent) return true; // No changes, exit
|
||||
|
||||
|
|
@ -132,6 +165,7 @@ class _NoteEditorScreenState extends State<NoteEditorScreen> {
|
|||
void dispose() {
|
||||
_controller.removeListener(_onTextChanged);
|
||||
_controller.dispose();
|
||||
_editorFocus.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
|
@ -151,6 +185,11 @@ class _NoteEditorScreenState extends State<NoteEditorScreen> {
|
|||
},
|
||||
),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
_isEditing ? Icons.edit_off_outlined : Icons.edit_outlined),
|
||||
onPressed: _isLoading ? null : _toggleEditing,
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 15),
|
||||
child: PopupMenuButton<String>(
|
||||
|
|
@ -161,6 +200,9 @@ class _NoteEditorScreenState extends State<NoteEditorScreen> {
|
|||
),
|
||||
onSelected: (String value) {
|
||||
switch (value) {
|
||||
case 'Share': // ✅ New case for sharing
|
||||
_shareNote();
|
||||
break;
|
||||
case 'Save':
|
||||
_saveNote();
|
||||
break;
|
||||
|
|
@ -175,6 +217,14 @@ class _NoteEditorScreenState extends State<NoteEditorScreen> {
|
|||
}
|
||||
},
|
||||
itemBuilder: (BuildContext context) => [
|
||||
PopupMenuItem<String>( // ✅ Share button
|
||||
value: 'Share',
|
||||
child: ListTile(
|
||||
leading: Icon(Icons.share_outlined),
|
||||
title: Text("Share"),
|
||||
),
|
||||
),
|
||||
PopupMenuDivider(),
|
||||
PopupMenuItem<String>(
|
||||
value: 'Save',
|
||||
child: ListTile(
|
||||
|
|
@ -193,6 +243,7 @@ class _NoteEditorScreenState extends State<NoteEditorScreen> {
|
|||
: "Enable Autosave"),
|
||||
),
|
||||
),
|
||||
PopupMenuDivider(),
|
||||
PopupMenuItem<String>(
|
||||
enabled: false,
|
||||
child: Column(
|
||||
|
|
@ -219,6 +270,7 @@ class _NoteEditorScreenState extends State<NoteEditorScreen> {
|
|||
],
|
||||
),
|
||||
),
|
||||
PopupMenuDivider(),
|
||||
PopupMenuItem<String>(
|
||||
value: 'Toggle Preview',
|
||||
child: ListTile(
|
||||
|
|
@ -288,20 +340,39 @@ class _NoteEditorScreenState extends State<NoteEditorScreen> {
|
|||
)
|
||||
: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: CodeTheme(
|
||||
data: CodeThemeData(
|
||||
styles: widget.isDarkMode ? nightTheme : dayTheme),
|
||||
child: SingleChildScrollView(
|
||||
child: WrappedCodeField(
|
||||
wrap: true,
|
||||
controller: _controller,
|
||||
textStyle: TextStyle(
|
||||
fontSize: _fontSize,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: MarkdownView(
|
||||
key: _viewKey,
|
||||
controller: _controller,
|
||||
readOnly: !_isEditing,
|
||||
fontSize: _fontSize,
|
||||
focusNode: _editorFocus,
|
||||
),
|
||||
),
|
||||
floatingActionButton: _isLoading || _isPreviewMode
|
||||
? null
|
||||
: ListenableBuilder(
|
||||
listenable: _controller,
|
||||
builder: (context, _) => _controller.matches.length > 1
|
||||
? Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
FloatingActionButton.small(
|
||||
heroTag: 'previous-match',
|
||||
onPressed: () =>
|
||||
_showMatch(_controller.currentMatch - 1),
|
||||
child: Icon(Icons.keyboard_arrow_up),
|
||||
),
|
||||
SizedBox(height: 8),
|
||||
FloatingActionButton.small(
|
||||
heroTag: 'next-match',
|
||||
onPressed: () =>
|
||||
_showMatch(_controller.currentMatch + 1),
|
||||
child: Icon(Icons.keyboard_arrow_down),
|
||||
),
|
||||
],
|
||||
)
|
||||
: SizedBox.shrink(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue