muzzle-velocity/lib/note_editor.dart

407 lines
13 KiB
Dart
Raw Normal View History

2025-02-21 22:51:56 +02:00
import 'dart:io';
2025-02-20 22:40:41 +02:00
import 'package:flutter/material.dart';
import 'package:flutter_code_editor/flutter_code_editor.dart';
2025-02-21 18:16:17 +02:00
import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:flutter_markdown_latex/flutter_markdown_latex.dart';
2025-02-22 19:11:10 +02:00
import 'package:flutter_svg/svg.dart';
2025-02-21 22:51:56 +02:00
import 'package:markdown/markdown.dart' as md;
2025-02-22 19:11:10 +02:00
import 'package:shared_preferences/shared_preferences.dart';
import 'package:share_plus/share_plus.dart';
2025-02-21 17:46:41 +02:00
import 'themes/day.dart';
import 'themes/night.dart';
import 'themes/markdown.dart';
2025-02-22 17:18:47 +02:00
import 'codefield.dart';
2025-02-20 22:40:41 +02:00
class NoteEditorScreen extends StatefulWidget {
final File note;
final String? notesDirectoryPath;
2025-02-21 22:51:56 +02:00
final bool isDarkMode;
final NoteMode initialMode;
final bool scrollToEnd;
const NoteEditorScreen({
super.key,
required this.note,
required this.isDarkMode,
required this.notesDirectoryPath,
this.initialMode = NoteMode.view,
this.scrollToEnd = false,
});
2025-02-20 22:40:41 +02:00
@override
State<NoteEditorScreen> createState() => _NoteEditorScreenState();
2025-02-20 22:40:41 +02:00
}
enum NoteMode { view, edit, preview }
2025-02-20 22:40:41 +02:00
class _NoteEditorScreenState extends State<NoteEditorScreen> {
late CodeController _controller;
2025-02-22 19:11:10 +02:00
late String _initialContent;
late NoteMode _mode;
final FocusNode _editorFocusNode = FocusNode();
final ScrollController _scrollController = ScrollController();
2025-02-22 19:11:10 +02:00
bool _isLoading = true;
bool _autoSaveEnabled = false;
2025-02-22 19:11:10 +02:00
double _fontSize = 16.0;
bool _cursorAtEnd = false;
2025-02-21 17:46:41 +02:00
2025-02-20 22:40:41 +02:00
@override
void initState() {
super.initState();
_mode = widget.initialMode;
_cursorAtEnd = widget.scrollToEnd;
2025-02-22 19:11:10 +02:00
_loadPreferences();
2025-02-20 22:40:41 +02:00
_loadNoteContent();
}
2025-02-22 19:11:10 +02:00
Future<void> _loadPreferences() async {
final prefs = await SharedPreferences.getInstance();
setState(() {
_autoSaveEnabled = prefs.getBool('auto_save_enabled') ?? false;
});
}
Future<void> _setAutoSaveEnabled(bool value) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setBool('auto_save_enabled', value);
setState(() {
_autoSaveEnabled = value;
});
}
2025-02-20 22:40:41 +02:00
Future<void> _loadNoteContent() async {
2025-02-21 22:02:06 +02:00
try {
2025-02-22 19:11:10 +02:00
_initialContent = await widget.note.readAsString();
2025-02-21 22:02:06 +02:00
} catch (e) {
debugPrint("❌ Failed to read file: ${widget.note.path}");
2025-02-22 19:11:10 +02:00
_initialContent = "";
2025-02-21 22:02:06 +02:00
}
2025-02-20 22:40:41 +02:00
setState(() {
_controller = CodeController(
2025-02-22 19:11:10 +02:00
text: _initialContent,
2025-02-20 22:40:41 +02:00
language: markdown,
);
2025-02-22 19:11:10 +02:00
_controller.addListener(_onTextChanged);
_isLoading = false;
2025-02-20 22:40:41 +02:00
});
if (widget.initialMode == NoteMode.edit) {
WidgetsBinding.instance.addPostFrameCallback((_) {
_controller.selection =
TextSelection.collapsed(offset: _controller.text.length);
_editorFocusNode.requestFocus();
});
} else if (widget.scrollToEnd) {
// Two frames: first renders the content, second has a stable maxScrollExtent.
WidgetsBinding.instance.addPostFrameCallback((_) {
WidgetsBinding.instance.addPostFrameCallback((_) => _scrollToBottom());
});
}
}
void _scrollToBottom() {
if (_scrollController.hasClients) {
_scrollController.jumpTo(_scrollController.position.maxScrollExtent);
}
2025-02-20 22:40:41 +02:00
}
2025-02-22 19:11:10 +02:00
void _onTextChanged() {
if (_mode == NoteMode.edit && _autoSaveEnabled && _controller.text != _initialContent) {
2025-02-22 19:11:10 +02:00
_saveNote();
}
}
2025-02-20 22:40:41 +02:00
Future<void> _saveNote() async {
2025-02-21 22:02:06 +02:00
try {
2025-02-22 19:11:10 +02:00
if (_controller.text == _initialContent) return; // No change, skip saving
2025-02-21 22:02:06 +02:00
2025-02-22 19:11:10 +02:00
await widget.note.writeAsString(_controller.text);
_initialContent = _controller.text; // Update initial state
2025-02-21 22:02:06 +02:00
debugPrint("✅ Note saved successfully");
2025-02-22 19:11:10 +02:00
} catch (e) {
debugPrint("❌ Error saving note: ${widget.note.path}");
}
}
void _shareNote() {
if (_controller.text.isNotEmpty) {
SharePlus.instance.share(ShareParams(
text: _controller.text,
subject: widget.note.uri.pathSegments.last,
));
2025-02-21 22:02:06 +02:00
}
2025-02-20 22:40:41 +02:00
}
2025-02-22 19:11:10 +02:00
Future<bool> _onWillPop() async {
if (_mode != NoteMode.edit || _controller.text == _initialContent) return true;
2025-02-22 19:11:10 +02:00
return await showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text("Save Changes?"),
content: Text("You have unsaved changes. Save before exiting?"),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(true), // Discard and exit
child: Text("Discard"),
),
TextButton(
onPressed: () async {
await _saveNote();
if (context.mounted) Navigator.of(context).pop(true);
2025-02-22 19:11:10 +02:00
},
child: Text("Save"),
),
TextButton(
onPressed: () => Navigator.of(context).pop(false), // Stay on page
child: Text("Cancel"),
),
],
),
) ??
false;
}
2025-02-21 22:51:56 +02:00
void _updateFontSize(double newSize) {
setState(() {
_fontSize = newSize;
});
}
2025-02-20 22:40:41 +02:00
@override
void dispose() {
2025-02-22 19:11:10 +02:00
_controller.removeListener(_onTextChanged);
2025-02-20 22:40:41 +02:00
_controller.dispose();
_editorFocusNode.dispose();
_scrollController.dispose();
2025-02-20 22:40:41 +02:00
super.dispose();
}
@override
Widget build(BuildContext context) {
return PopScope(
canPop: false,
onPopInvokedWithResult: (bool didPop, Object? result) async {
if (didPop) return;
final bool shouldPop = await _onWillPop();
if (shouldPop && context.mounted) Navigator.of(context).pop();
},
2025-02-22 19:11:10 +02:00
child: Scaffold(
appBar: AppBar(
title: Text(widget.note.uri.pathSegments.last.replaceAll('.md', '')),
leading: IconButton(
icon: Icon(Icons.arrow_back),
onPressed: () async {
if (await _onWillPop()) {
if (context.mounted) Navigator.pop(context);
2025-02-22 19:11:10 +02:00
}
},
),
actions: [
Padding(
padding: EdgeInsets.symmetric(horizontal: 15),
child: PopupMenuButton<String>(
icon: SvgPicture.asset(
"assets/blaster.svg",
width: 30,
colorFilter: const ColorFilter.mode(Colors.orange, BlendMode.srcIn),
2025-02-21 17:46:41 +02:00
),
2025-02-22 19:11:10 +02:00
onSelected: (String value) {
switch (value) {
case 'Share': // ✅ New case for sharing
_shareNote();
break;
2025-02-22 19:11:10 +02:00
case 'Save':
_saveNote();
break;
case 'Toggle Autosave':
_setAutoSaveEnabled(!_autoSaveEnabled);
break;
case 'Mode View':
setState(() { _mode = NoteMode.view; });
break;
case 'Mode Edit':
setState(() { _mode = NoteMode.edit; });
if (_cursorAtEnd) {
_cursorAtEnd = false; // consume once
WidgetsBinding.instance.addPostFrameCallback((_) {
_controller.selection = TextSelection.collapsed(
offset: _controller.text.length);
_editorFocusNode.requestFocus();
_scrollToBottom();
});
}
break;
case 'Mode Preview':
setState(() { _mode = NoteMode.preview; });
2025-02-22 19:11:10 +02:00
break;
}
},
itemBuilder: (BuildContext context) => [
PopupMenuItem<String>( // ✅ Share button
value: 'Share',
child: ListTile(
leading: Icon(Icons.share_outlined),
title: Text("Share"),
),
),
PopupMenuDivider(),
2025-02-22 19:11:10 +02:00
PopupMenuItem<String>(
value: 'Save',
enabled: _mode == NoteMode.edit,
2025-02-22 19:11:10 +02:00
child: ListTile(
leading: Icon(Icons.save_outlined),
2025-02-22 19:11:10 +02:00
title: Text("Save"),
),
2025-02-21 18:16:17 +02:00
),
2025-02-22 19:11:10 +02:00
PopupMenuItem<String>(
value: 'Toggle Autosave',
enabled: _mode == NoteMode.edit,
2025-02-22 19:11:10 +02:00
child: ListTile(
leading: Icon(_autoSaveEnabled
? Icons.alarm_off
: Icons.alarm_on),
2025-02-22 19:11:10 +02:00
title: Text(_autoSaveEnabled
? "Disable Autosave"
: "Enable Autosave"),
2025-02-21 22:51:56 +02:00
),
),
PopupMenuDivider(),
PopupMenuItem<String>(
enabled: false,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Font Size'),
StatefulBuilder(
builder: (context, setState) {
return Slider(
value: _fontSize,
min: 12.0,
max: 24.0,
divisions: 6,
label: _fontSize.toString(),
onChanged: (newSize) {
setState(() {
_fontSize = newSize;
});
_updateFontSize(newSize);
},
);
},
),
],
),
),
PopupMenuDivider(),
2025-02-22 19:11:10 +02:00
PopupMenuItem<String>(
value: 'Mode View',
child: ListTile(
leading: Icon(_mode == NoteMode.view
? Icons.check
: Icons.book_outlined),
title: Text("View Mode"),
),
),
PopupMenuItem<String>(
value: 'Mode Edit',
child: ListTile(
leading: Icon(_mode == NoteMode.edit
? Icons.check
: Icons.edit_note_outlined),
title: Text("Edit Mode"),
),
),
PopupMenuItem<String>(
value: 'Mode Preview',
2025-02-22 19:11:10 +02:00
child: ListTile(
leading: Icon(_mode == NoteMode.preview
? Icons.check
: Icons.visibility_outlined),
title: Text("Preview Mode"),
2025-02-21 22:51:56 +02:00
),
),
2025-02-22 19:11:10 +02:00
],
),
),
],
),
body: _isLoading
? Center(child: CircularProgressIndicator())
: _mode == NoteMode.preview
2025-02-22 19:11:10 +02:00
? Padding(
padding: const EdgeInsets.all(16.0),
child: SingleChildScrollView(
child: MarkdownBody(
builders: {
'latex': LatexElementBuilder(
textStyle: TextStyle(
color: widget.isDarkMode
? Colors.white
: Colors.black)),
},
extensionSet: md.ExtensionSet(
[LatexBlockSyntax()],
[LatexInlineSyntax()],
),
data: _controller.text,
selectable: true,
styleSheet: MarkdownStyleSheet(
p: TextStyle(
fontSize: _fontSize,
color: widget.isDarkMode
? Colors.white
: Colors.black),
h1: TextStyle(
fontSize: _fontSize + 8,
color: widget.isDarkMode
? Colors.white
: Colors.black,
fontWeight: FontWeight.bold),
h2: TextStyle(
fontSize: _fontSize + 6,
color: widget.isDarkMode
? Colors.white
: Colors.black,
fontWeight: FontWeight.bold),
h3: TextStyle(
fontSize: _fontSize + 4,
color: widget.isDarkMode
? Colors.white
: Colors.black,
fontWeight: FontWeight.bold),
code: TextStyle(
fontSize: _fontSize,
backgroundColor: Colors.transparent),
codeblockDecoration: BoxDecoration(
color: Colors.transparent),
),
),
),
)
: Padding(
padding: const EdgeInsets.all(16.0),
child: CodeTheme(
data: CodeThemeData(
styles: widget.isDarkMode ? nightTheme : dayTheme),
child: SingleChildScrollView(
controller: _scrollController,
2025-02-22 19:11:10 +02:00
child: WrappedCodeField(
wrap: true,
controller: _controller,
readOnly: _mode == NoteMode.view,
focusNode: _editorFocusNode,
2025-02-22 19:11:10 +02:00
textStyle: TextStyle(
fontSize: _fontSize,
2025-02-21 22:51:56 +02:00
),
2025-02-22 19:11:10 +02:00
),
),
),
),
),
2025-02-20 22:40:41 +02:00
);
}
}