app icons
This commit is contained in:
parent
be43333da6
commit
4fcd6db6fd
30 changed files with 331 additions and 216 deletions
|
|
@ -3,8 +3,9 @@ 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:path/path.dart' as path;
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'themes/day.dart';
|
||||
import 'themes/night.dart';
|
||||
import 'themes/markdown.dart';
|
||||
|
|
@ -25,57 +26,102 @@ class NoteEditorScreen extends StatefulWidget {
|
|||
}
|
||||
|
||||
class _NoteEditorScreenState extends State<NoteEditorScreen> {
|
||||
bool isLoading = true;
|
||||
bool _isPreviewMode = false; // Default: Editing mode
|
||||
late CodeController _controller;
|
||||
double _fontSize = 16.0; // Default font size
|
||||
late String _initialContent;
|
||||
bool _isPreviewMode = false;
|
||||
bool _isLoading = true;
|
||||
bool _autoSaveEnabled = false; // Controlled by user toggle
|
||||
double _fontSize = 16.0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadPreferences();
|
||||
_loadNoteContent();
|
||||
}
|
||||
|
||||
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;
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _loadNoteContent() async {
|
||||
String content = "";
|
||||
try {
|
||||
content = await widget.note.readAsString();
|
||||
_initialContent = await widget.note.readAsString();
|
||||
} catch (e) {
|
||||
print("❌ Failed to read file: ${widget.note.path}");
|
||||
_initialContent = "";
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_controller = CodeController(
|
||||
text: content,
|
||||
text: _initialContent,
|
||||
language: markdown,
|
||||
);
|
||||
_controller.addListener(_saveNote);
|
||||
_controller.popupController.enabled = false;
|
||||
isLoading = false;
|
||||
_controller.addListener(_onTextChanged);
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
|
||||
void _onTextChanged() {
|
||||
if (_autoSaveEnabled && _controller.text != _initialContent) {
|
||||
_saveNote();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _saveNote() async {
|
||||
try {
|
||||
final notesDirectory = widget.notesDirectoryPath ??
|
||||
widget.note.parent.path; // ✅ Use correct directory
|
||||
if (_controller.text == _initialContent) return; // No change, skip saving
|
||||
|
||||
// Get the full path of the original file, preserving subfolder structure
|
||||
final relativeNotePath = path.relative(widget.note.path, from: notesDirectory);
|
||||
final newFilePath = path.join(notesDirectory, relativeNotePath); // ✅ Preserve subfolder
|
||||
await widget.note.writeAsString(_controller.text);
|
||||
_initialContent = _controller.text; // Update initial state
|
||||
|
||||
print("📝 Attempting to save note at: $newFilePath");
|
||||
final newFile = File(newFilePath);
|
||||
await newFile.writeAsString(_controller.text);
|
||||
|
||||
print("✅ Successfully saved note at: $newFilePath");
|
||||
} catch (e, stackTrace) {
|
||||
print("✅ Note saved successfully");
|
||||
} catch (e) {
|
||||
print("❌ Error saving note: ${widget.note.path}");
|
||||
print("⚠️ Exception: $e");
|
||||
print("🛠 Stack trace: $stackTrace");
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> _onWillPop() async {
|
||||
if (_controller.text == _initialContent) return true; // No changes, exit
|
||||
|
||||
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();
|
||||
Navigator.of(context).pop(true); // Save and exit
|
||||
},
|
||||
child: Text("Save"),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(false), // Stay on page
|
||||
child: Text("Cancel"),
|
||||
),
|
||||
],
|
||||
),
|
||||
) ??
|
||||
false;
|
||||
}
|
||||
|
||||
void _updateFontSize(double newSize) {
|
||||
setState(() {
|
||||
_fontSize = newSize;
|
||||
|
|
@ -84,138 +130,153 @@ class _NoteEditorScreenState extends State<NoteEditorScreen> {
|
|||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.removeListener(_saveNote);
|
||||
_controller.removeListener(_onTextChanged);
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(widget.note.uri.pathSegments.last.replaceAll('.md', '')),
|
||||
actions: [
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 15),
|
||||
child: PopupMenuButton<String>(
|
||||
icon: Icon(Icons.radio_button_checked, color: Colors.orange),
|
||||
onSelected: (String value) {
|
||||
if (value == 'Toggle Preview') {
|
||||
setState(() {
|
||||
_isPreviewMode = !_isPreviewMode;
|
||||
});
|
||||
}
|
||||
return WillPopScope(
|
||||
onWillPop: _onWillPop,
|
||||
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()) {
|
||||
Navigator.pop(context);
|
||||
}
|
||||
},
|
||||
),
|
||||
actions: [
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 15),
|
||||
child: PopupMenuButton<String>(
|
||||
icon: SvgPicture.asset(
|
||||
"assets/blaster.svg",
|
||||
width: 30,
|
||||
color: Colors.orange,
|
||||
),
|
||||
onSelected: (String value) {
|
||||
switch (value) {
|
||||
case 'Save':
|
||||
_saveNote();
|
||||
break;
|
||||
case 'Toggle Autosave':
|
||||
_setAutoSaveEnabled(!_autoSaveEnabled);
|
||||
break;
|
||||
case 'Toggle Preview':
|
||||
setState(() {
|
||||
_isPreviewMode = !_isPreviewMode;
|
||||
});
|
||||
break;
|
||||
}
|
||||
},
|
||||
itemBuilder: (BuildContext context) => [
|
||||
PopupMenuItem<String>(
|
||||
value: 'Save',
|
||||
child: ListTile(
|
||||
leading: Icon(Icons.save),
|
||||
title: Text("Save"),
|
||||
),
|
||||
),
|
||||
PopupMenuItem<String>(
|
||||
value: 'Toggle Autosave',
|
||||
child: ListTile(
|
||||
leading: Icon(_autoSaveEnabled
|
||||
? Icons.toggle_on
|
||||
: Icons.toggle_off),
|
||||
title: Text(_autoSaveEnabled
|
||||
? "Disable Autosave"
|
||||
: "Enable Autosave"),
|
||||
),
|
||||
),
|
||||
PopupMenuItem<String>(
|
||||
value: 'Toggle Preview',
|
||||
child: ListTile(
|
||||
leading:
|
||||
Icon(_isPreviewMode ? Icons.edit : Icons.preview),
|
||||
title:
|
||||
Text(_isPreviewMode ? "Edit Mode" : "Preview Mode"),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: _isLoading
|
||||
? Center(child: CircularProgressIndicator())
|
||||
: _isPreviewMode
|
||||
? Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: SingleChildScrollView(
|
||||
child: MarkdownBody(
|
||||
builders: {
|
||||
'latex': LatexElementBuilder(
|
||||
textStyle: TextStyle(
|
||||
color: widget.isDarkMode
|
||||
? Colors.white
|
||||
: Colors.black)),
|
||||
},
|
||||
itemBuilder: (BuildContext context) => [
|
||||
PopupMenuItem<String>(
|
||||
value: 'Toggle Preview',
|
||||
child: ListTile(
|
||||
leading: Icon(_isPreviewMode ? Icons.edit : Icons.preview),
|
||||
title: Text(_isPreviewMode ? 'Edit Mode' : 'Preview Mode'),
|
||||
),
|
||||
),
|
||||
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);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
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),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: isLoading
|
||||
? Center(
|
||||
child:
|
||||
CircularProgressIndicator()) // Show loader while initializing
|
||||
: _isPreviewMode
|
||||
? 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, // ✅ Plain Markdown without syntax highlighting
|
||||
selectable: true, // ✅ Allows text selection
|
||||
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), // ✅ Disables syntax highlighting
|
||||
codeblockDecoration: BoxDecoration(
|
||||
color: Colors
|
||||
.transparent), // ✅ Removes background color from code blocks
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
: 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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
: 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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import 'dart:io';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:open_filex/open_filex.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
|
|
@ -396,6 +397,7 @@ class _NoteListScreenState extends State<NoteListScreen> {
|
|||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
duration: const Duration(seconds: 3),
|
||||
content: Text('Note moved to trash'),
|
||||
action: SnackBarAction(
|
||||
label: 'Undo',
|
||||
|
|
@ -502,6 +504,7 @@ class _NoteListScreenState extends State<NoteListScreen> {
|
|||
autocorrect: false,
|
||||
textInputAction: TextInputAction.done,
|
||||
controller: searchController,
|
||||
cursorColor: Colors.orange,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Search or enter a command...',
|
||||
border: InputBorder.none,
|
||||
|
|
@ -521,8 +524,11 @@ class _NoteListScreenState extends State<NoteListScreen> {
|
|||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 15),
|
||||
child: PopupMenuButton<String>(
|
||||
icon: Icon(Icons.radio_button_checked,
|
||||
color: Colors.orange), // Menu button (⋮)
|
||||
icon: SvgPicture.asset(
|
||||
"assets/blaster.svg",
|
||||
width: 30,
|
||||
color: Colors.orange,
|
||||
),
|
||||
onSelected: (String value) {
|
||||
switch (value) {
|
||||
case 'Toggle Tags':
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue