mirror of
https://github.com/GitJournal/GitJournal.git
synced 2025-06-27 01:02:14 +08:00
Add the concept of Editors
* We no longer have a separate editing and browsing view - This does mean we loose the ability to quick flip between notes by swiping. However, this is more how a note editor would behave. I do later want to add that capability back. * We have 2 editors for now - Markdown and Raw. By default we use the Markdown editor which can be toggled between Preview / Edit mode. I later want to add a rich text editor and a todo editor as well.
This commit is contained in:
@ -156,9 +156,17 @@ class Note with ChangeNotifier implements Comparable<Note> {
|
||||
}
|
||||
|
||||
void rename(String newName) {
|
||||
// Do not let the user rename it to a non-markdown file
|
||||
if (!newName.toLowerCase().endsWith('.md')) {
|
||||
newName += '.md';
|
||||
}
|
||||
|
||||
var parentDirName = p.dirname(filePath);
|
||||
var newFilePath = p.join(parentDirName, newName);
|
||||
File(filePath).renameSync(newFilePath);
|
||||
if (_loadState != NoteLoadState.None) {
|
||||
// for new notes
|
||||
File(filePath).renameSync(newFilePath);
|
||||
}
|
||||
_filePath = newFilePath;
|
||||
|
||||
notifyListeners();
|
||||
|
198
lib/editors/markdown_editor.dart
Normal file
198
lib/editors/markdown_editor.dart
Normal file
@ -0,0 +1,198 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:gitjournal/core/note.dart';
|
||||
import 'package:gitjournal/widgets/note_viewer.dart';
|
||||
|
||||
typedef NoteCallback = void Function(Note);
|
||||
enum DropDownChoices { Rename }
|
||||
|
||||
class MarkdownEditor extends StatefulWidget {
|
||||
final Note note;
|
||||
final NoteCallback noteDeletionSelected;
|
||||
final NoteCallback noteEditorChooserSelected;
|
||||
final NoteCallback exitEditorSelected;
|
||||
final NoteCallback renameNoteSelected;
|
||||
final bool openEditorByDefault;
|
||||
|
||||
MarkdownEditor({
|
||||
Key key,
|
||||
@required this.note,
|
||||
@required this.noteDeletionSelected,
|
||||
@required this.noteEditorChooserSelected,
|
||||
@required this.exitEditorSelected,
|
||||
@required this.renameNoteSelected,
|
||||
this.openEditorByDefault = false,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
MarkdownEditorState createState() {
|
||||
return MarkdownEditorState(note);
|
||||
}
|
||||
}
|
||||
|
||||
class MarkdownEditorState extends State<MarkdownEditor> {
|
||||
Note note;
|
||||
TextEditingController _textController = TextEditingController();
|
||||
TextEditingController _titleTextController = TextEditingController();
|
||||
|
||||
bool editingMode = false;
|
||||
|
||||
MarkdownEditorState(this.note) {
|
||||
_textController = TextEditingController(text: note.body);
|
||||
_titleTextController = TextEditingController(text: note.title);
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
editingMode = widget.openEditorByDefault;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_textController.dispose();
|
||||
_titleTextController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var editor = Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
_NoteTitleEditor(_titleTextController),
|
||||
_NoteBodyEditor(_textController),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
Widget body = editingMode ? editor : NoteViewer(note: note);
|
||||
var fab = FloatingActionButton(
|
||||
child: editingMode
|
||||
? const Icon(Icons.remove_red_eye)
|
||||
: const Icon(Icons.edit),
|
||||
onPressed: _switchMode,
|
||||
);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
leading: IconButton(
|
||||
key: const ValueKey("NewEntry"),
|
||||
icon: const Icon(Icons.check),
|
||||
onPressed: () {
|
||||
_updateNote();
|
||||
widget.exitEditorSelected(note);
|
||||
},
|
||||
),
|
||||
actions: <Widget>[
|
||||
IconButton(
|
||||
icon: editingMode
|
||||
? const Icon(Icons.remove_red_eye)
|
||||
: const Icon(Icons.edit),
|
||||
onPressed: _switchMode,
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.library_books),
|
||||
onPressed: () {
|
||||
_updateNote();
|
||||
widget.noteEditorChooserSelected(note);
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.delete),
|
||||
onPressed: () {
|
||||
_updateNote();
|
||||
widget.noteDeletionSelected(note);
|
||||
},
|
||||
),
|
||||
PopupMenuButton<DropDownChoices>(
|
||||
onSelected: (DropDownChoices choice) {
|
||||
_updateNote();
|
||||
widget.renameNoteSelected(note);
|
||||
},
|
||||
itemBuilder: (BuildContext context) =>
|
||||
<PopupMenuEntry<DropDownChoices>>[
|
||||
const PopupMenuItem<DropDownChoices>(
|
||||
value: DropDownChoices.Rename,
|
||||
child: Text('Edit File Name'),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
body: body,
|
||||
floatingActionButton: fab,
|
||||
);
|
||||
}
|
||||
|
||||
void _switchMode() {
|
||||
setState(() {
|
||||
editingMode = !editingMode;
|
||||
_updateNote();
|
||||
});
|
||||
}
|
||||
|
||||
void _updateNote() {
|
||||
note.title = _titleTextController.text.trim();
|
||||
note.body = _textController.text.trim();
|
||||
}
|
||||
|
||||
Note getNote() {
|
||||
_updateNote();
|
||||
return note;
|
||||
}
|
||||
}
|
||||
|
||||
class _NoteBodyEditor extends StatelessWidget {
|
||||
final TextEditingController textController;
|
||||
|
||||
_NoteBodyEditor(this.textController);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var style = Theme.of(context).textTheme.subhead;
|
||||
|
||||
return TextField(
|
||||
autofocus: true,
|
||||
autocorrect: false,
|
||||
keyboardType: TextInputType.multiline,
|
||||
maxLines: null,
|
||||
style: style,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Write here',
|
||||
border: InputBorder.none,
|
||||
isDense: true,
|
||||
),
|
||||
controller: textController,
|
||||
textCapitalization: TextCapitalization.sentences,
|
||||
scrollPadding: const EdgeInsets.all(0.0),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _NoteTitleEditor extends StatelessWidget {
|
||||
final TextEditingController textController;
|
||||
|
||||
_NoteTitleEditor(this.textController);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var style = Theme.of(context).textTheme.title;
|
||||
|
||||
return TextField(
|
||||
keyboardType: TextInputType.text,
|
||||
maxLines: 1,
|
||||
style: style,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Title',
|
||||
border: InputBorder.none,
|
||||
isDense: true,
|
||||
),
|
||||
controller: textController,
|
||||
textCapitalization: TextCapitalization.sentences,
|
||||
);
|
||||
}
|
||||
}
|
136
lib/editors/raw_editor.dart
Normal file
136
lib/editors/raw_editor.dart
Normal file
@ -0,0 +1,136 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:gitjournal/core/note.dart';
|
||||
import 'package:gitjournal/core/note_data_serializers.dart';
|
||||
|
||||
typedef NoteCallback = void Function(Note);
|
||||
enum DropDownChoices { Rename }
|
||||
|
||||
class RawEditor extends StatefulWidget {
|
||||
final Note note;
|
||||
final NoteCallback noteDeletionSelected;
|
||||
final NoteCallback noteEditorChooserSelected;
|
||||
final NoteCallback exitEditorSelected;
|
||||
final NoteCallback renameNoteSelected;
|
||||
|
||||
RawEditor({
|
||||
Key key,
|
||||
@required this.note,
|
||||
@required this.noteDeletionSelected,
|
||||
@required this.noteEditorChooserSelected,
|
||||
@required this.exitEditorSelected,
|
||||
@required this.renameNoteSelected,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
RawEditorState createState() {
|
||||
return RawEditorState(note);
|
||||
}
|
||||
}
|
||||
|
||||
class RawEditorState extends State<RawEditor> {
|
||||
Note note;
|
||||
TextEditingController _textController = TextEditingController();
|
||||
|
||||
final serializer = MarkdownYAMLSerializer();
|
||||
|
||||
RawEditorState(this.note) {
|
||||
_textController = TextEditingController(text: serializer.encode(note.data));
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_textController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var editor = Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: SingleChildScrollView(
|
||||
child: _NoteEditor(_textController),
|
||||
),
|
||||
);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
leading: IconButton(
|
||||
key: const ValueKey("NewEntry"),
|
||||
icon: const Icon(Icons.check),
|
||||
onPressed: () {
|
||||
_updateNote();
|
||||
widget.exitEditorSelected(note);
|
||||
},
|
||||
),
|
||||
actions: <Widget>[
|
||||
IconButton(
|
||||
icon: const Icon(Icons.library_books),
|
||||
onPressed: () {
|
||||
_updateNote();
|
||||
widget.noteEditorChooserSelected(note);
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.delete),
|
||||
onPressed: () {
|
||||
_updateNote();
|
||||
widget.noteDeletionSelected(note);
|
||||
},
|
||||
),
|
||||
PopupMenuButton<DropDownChoices>(
|
||||
onSelected: (DropDownChoices choice) {
|
||||
_updateNote();
|
||||
widget.renameNoteSelected(note);
|
||||
},
|
||||
itemBuilder: (BuildContext context) =>
|
||||
<PopupMenuEntry<DropDownChoices>>[
|
||||
const PopupMenuItem<DropDownChoices>(
|
||||
value: DropDownChoices.Rename,
|
||||
child: Text('Edit File Name'),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
body: editor,
|
||||
);
|
||||
}
|
||||
|
||||
void _updateNote() {
|
||||
note.data = serializer.decode(_textController.text);
|
||||
}
|
||||
|
||||
Note getNote() {
|
||||
_updateNote();
|
||||
return note;
|
||||
}
|
||||
}
|
||||
|
||||
class _NoteEditor extends StatelessWidget {
|
||||
final TextEditingController textController;
|
||||
|
||||
_NoteEditor(this.textController);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var style =
|
||||
Theme.of(context).textTheme.subhead.copyWith(fontFamily: "Roboto Mono");
|
||||
|
||||
return TextField(
|
||||
autofocus: false,
|
||||
autocorrect: false,
|
||||
keyboardType: TextInputType.multiline,
|
||||
maxLines: null,
|
||||
style: style,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Write here',
|
||||
border: InputBorder.none,
|
||||
isDense: true,
|
||||
),
|
||||
controller: textController,
|
||||
textCapitalization: TextCapitalization.sentences,
|
||||
scrollPadding: const EdgeInsets.all(0.0),
|
||||
);
|
||||
}
|
||||
}
|
@ -1,162 +0,0 @@
|
||||
import 'package:fimber/fimber.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:page_transition/page_transition.dart';
|
||||
import 'package:share/share.dart';
|
||||
|
||||
import 'package:gitjournal/core/note.dart';
|
||||
import 'package:gitjournal/state_container.dart';
|
||||
import 'package:gitjournal/utils.dart';
|
||||
import 'package:gitjournal/widgets/rename_dialog.dart';
|
||||
import 'package:gitjournal/widgets/note_viewer.dart';
|
||||
|
||||
import 'journal_editor.dart';
|
||||
|
||||
enum NoteBrowserDropDownChoices { Rename }
|
||||
|
||||
class JournalBrowsingScreen extends StatefulWidget {
|
||||
final List<Note> notes;
|
||||
final int noteIndex;
|
||||
|
||||
const JournalBrowsingScreen({
|
||||
@required this.notes,
|
||||
@required this.noteIndex,
|
||||
});
|
||||
|
||||
@override
|
||||
JournalBrowsingScreenState createState() {
|
||||
return JournalBrowsingScreenState(noteIndex: noteIndex);
|
||||
}
|
||||
}
|
||||
|
||||
class JournalBrowsingScreenState extends State<JournalBrowsingScreen> {
|
||||
PageController pageController;
|
||||
int currentPage;
|
||||
|
||||
JournalBrowsingScreenState({@required int noteIndex}) {
|
||||
pageController = PageController(initialPage: noteIndex);
|
||||
currentPage = noteIndex;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var pageView = PageView.builder(
|
||||
controller: pageController,
|
||||
itemCount: widget.notes.length,
|
||||
itemBuilder: (BuildContext context, int pos) {
|
||||
var note = widget.notes[pos];
|
||||
return NoteViewer(
|
||||
key: ValueKey("Viewer_" + note.filePath),
|
||||
note: widget.notes[pos],
|
||||
);
|
||||
},
|
||||
onPageChanged: (int pageNum) {
|
||||
setState(() {
|
||||
currentPage = pageNum;
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
actions: <Widget>[
|
||||
IconButton(
|
||||
icon: Icon(Icons.delete),
|
||||
onPressed: () {
|
||||
showDialog(context: context, builder: _buildAlertDialog);
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(Icons.share),
|
||||
onPressed: () {
|
||||
Note note = widget.notes[_currentIndex()];
|
||||
Share.share(note.body);
|
||||
},
|
||||
),
|
||||
PopupMenuButton<NoteBrowserDropDownChoices>(
|
||||
onSelected: (NoteBrowserDropDownChoices choice) async {
|
||||
var note = widget.notes[currentPage];
|
||||
switch (choice) {
|
||||
case NoteBrowserDropDownChoices.Rename:
|
||||
var fileName = await showDialog(
|
||||
context: context,
|
||||
builder: (_) => RenameDialog(
|
||||
oldPath: note.filePath,
|
||||
inputDecoration: 'File Name',
|
||||
dialogTitle: "Rename File",
|
||||
),
|
||||
);
|
||||
if (fileName is String) {
|
||||
final container = StateContainer.of(context);
|
||||
container.renameNote(note, fileName);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
},
|
||||
itemBuilder: (BuildContext context) =>
|
||||
<PopupMenuEntry<NoteBrowserDropDownChoices>>[
|
||||
const PopupMenuItem<NoteBrowserDropDownChoices>(
|
||||
value: NoteBrowserDropDownChoices.Rename,
|
||||
child: Text('Rename File'),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
body: pageView,
|
||||
floatingActionButton: FloatingActionButton(
|
||||
child: Icon(Icons.edit),
|
||||
onPressed: () {
|
||||
Note note = widget.notes[_currentIndex()];
|
||||
Navigator.push(
|
||||
context,
|
||||
PageTransition(
|
||||
type: PageTransitionType.fade,
|
||||
child: JournalEditor.fromNote(note),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
int _currentIndex() {
|
||||
int currentIndex = pageController.page.round();
|
||||
assert(currentIndex >= 0);
|
||||
assert(currentIndex < widget.notes.length);
|
||||
return currentIndex;
|
||||
}
|
||||
|
||||
void _deleteNote(BuildContext context) {
|
||||
final stateContainer = StateContainer.of(context);
|
||||
var noteIndex = _currentIndex();
|
||||
Note note = widget.notes[noteIndex];
|
||||
stateContainer.removeNote(note);
|
||||
Navigator.pop(context);
|
||||
|
||||
Fimber.d("Shwoing an undo snackbar");
|
||||
showUndoDeleteSnackbar(context, stateContainer, note, noteIndex);
|
||||
}
|
||||
|
||||
Widget _buildAlertDialog(BuildContext context) {
|
||||
var title = "Are you sure you want to delete this Note?";
|
||||
|
||||
return AlertDialog(
|
||||
content: Text(title),
|
||||
actions: <Widget>[
|
||||
FlatButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
FlatButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(context); // Alert box
|
||||
_deleteNote(context);
|
||||
},
|
||||
child: const Text('Delete'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -1,273 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:gitjournal/core/note.dart';
|
||||
import 'package:gitjournal/core/notes_folder.dart';
|
||||
import 'package:gitjournal/state_container.dart';
|
||||
import 'package:gitjournal/core/note_data.dart';
|
||||
import 'package:gitjournal/core/note_data_serializers.dart';
|
||||
import 'package:gitjournal/widgets/note_viewer.dart';
|
||||
|
||||
enum NoteEditorDropDownChoices { Discard, SwitchEditor }
|
||||
|
||||
class JournalEditor extends StatefulWidget {
|
||||
final Note note;
|
||||
final NotesFolder notesFolder;
|
||||
|
||||
JournalEditor.fromNote(this.note) : notesFolder = null;
|
||||
JournalEditor.newNote(this.notesFolder) : note = null;
|
||||
|
||||
@override
|
||||
JournalEditorState createState() {
|
||||
if (note == null) {
|
||||
return JournalEditorState.newNote(notesFolder);
|
||||
} else {
|
||||
return JournalEditorState.fromNote(note);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class JournalEditorState extends State<JournalEditor> {
|
||||
Note note;
|
||||
final bool newNote;
|
||||
TextEditingController _textController = TextEditingController();
|
||||
TextEditingController _titleTextController = TextEditingController();
|
||||
|
||||
bool rawEditor = false;
|
||||
bool editingMode = false;
|
||||
final serializer = MarkdownYAMLSerializer();
|
||||
|
||||
JournalEditorState.newNote(NotesFolder folder) : newNote = true {
|
||||
note = Note.newNote(folder);
|
||||
}
|
||||
|
||||
JournalEditorState.fromNote(this.note) : newNote = false {
|
||||
_textController = TextEditingController(text: note.body);
|
||||
_titleTextController = TextEditingController(text: note.title);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_textController.dispose();
|
||||
_titleTextController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var markdownEditor = Column(
|
||||
children: <Widget>[
|
||||
if (!rawEditor) NoteTitleEditor(_titleTextController),
|
||||
NoteMarkdownEditor(_textController, false),
|
||||
],
|
||||
);
|
||||
|
||||
var rawEditorWidget = NoteMarkdownEditor(_textController, true);
|
||||
|
||||
var editorW = rawEditor ? rawEditorWidget : markdownEditor;
|
||||
var editor = Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: SingleChildScrollView(child: editorW),
|
||||
);
|
||||
|
||||
Widget body = editingMode ? editor : NoteViewer(note: note);
|
||||
|
||||
var newJournalScreen = Scaffold(
|
||||
appBar: AppBar(
|
||||
leading: IconButton(
|
||||
key: const ValueKey("NewEntry"),
|
||||
icon: const Icon(Icons.check),
|
||||
onPressed: () {
|
||||
_saveNote(context);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
actions: <Widget>[
|
||||
!rawEditor
|
||||
? IconButton(
|
||||
icon: editingMode
|
||||
? Icon(Icons.remove_red_eye)
|
||||
: Icon(Icons.edit),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
editingMode = !editingMode;
|
||||
});
|
||||
},
|
||||
)
|
||||
: Container(),
|
||||
PopupMenuButton<NoteEditorDropDownChoices>(
|
||||
onSelected: (NoteEditorDropDownChoices choice) {
|
||||
switch (choice) {
|
||||
case NoteEditorDropDownChoices.Discard:
|
||||
if (_noteModified()) {
|
||||
showDialog(context: context, builder: _buildAlertDialog);
|
||||
} else {
|
||||
Navigator.pop(context);
|
||||
}
|
||||
break;
|
||||
case NoteEditorDropDownChoices.SwitchEditor:
|
||||
note.title = _titleTextController.text.trim();
|
||||
setState(() {
|
||||
if (rawEditor) {
|
||||
rawEditor = false;
|
||||
note.data = serializer.decode(_textController.text);
|
||||
_textController.text = note.body;
|
||||
_titleTextController.text = note.title;
|
||||
} else {
|
||||
rawEditor = true;
|
||||
var noteData =
|
||||
NoteData(_textController.text, note.data.props);
|
||||
_textController.text = serializer.encode(noteData);
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
},
|
||||
itemBuilder: (BuildContext context) =>
|
||||
<PopupMenuEntry<NoteEditorDropDownChoices>>[
|
||||
const PopupMenuItem<NoteEditorDropDownChoices>(
|
||||
value: NoteEditorDropDownChoices.Discard,
|
||||
child: Text('Discard'),
|
||||
),
|
||||
PopupMenuItem<NoteEditorDropDownChoices>(
|
||||
value: NoteEditorDropDownChoices.SwitchEditor,
|
||||
child: rawEditor
|
||||
? const Text('Rich Editor')
|
||||
: const Text('Raw Editor'),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
body: body,
|
||||
floatingActionButton: FloatingActionButton(
|
||||
child: const Icon(Icons.check),
|
||||
onPressed: () {
|
||||
_saveNote(context);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
return WillPopScope(
|
||||
onWillPop: () async {
|
||||
_saveNote(context);
|
||||
return true;
|
||||
},
|
||||
child: newJournalScreen,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAlertDialog(BuildContext context) {
|
||||
var title = newNote
|
||||
? "Do you want to discard this?"
|
||||
: "Do you want to ignore the changes?";
|
||||
|
||||
var editText = newNote ? "Keep Writing" : "Keep Editing";
|
||||
var discardText = newNote ? "Discard" : "Discard Changes";
|
||||
|
||||
return AlertDialog(
|
||||
title: Text(title),
|
||||
actions: <Widget>[
|
||||
FlatButton(
|
||||
onPressed: () => Navigator.of(context).pop(false),
|
||||
child: Text(editText),
|
||||
),
|
||||
FlatButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(context); // Alert box
|
||||
Navigator.pop(context); // Note Editor
|
||||
},
|
||||
child: Text(discardText),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
bool _noteModified() {
|
||||
var noteContent = _textController.text.trim();
|
||||
var titleContent = _titleTextController.text.trim();
|
||||
if (noteContent.isEmpty && titleContent.isEmpty) {
|
||||
return false;
|
||||
}
|
||||
if (note != null) {
|
||||
if (rawEditor) {
|
||||
return serializer.encode(note.data) != noteContent;
|
||||
} else {
|
||||
return noteContent != note.body || titleContent != note.title;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void _saveNote(BuildContext context) {
|
||||
if (!_noteModified()) return;
|
||||
|
||||
final stateContainer = StateContainer.of(context);
|
||||
if (rawEditor == false) {
|
||||
note.body = _textController.text.trim();
|
||||
note.title = _titleTextController.text.trim();
|
||||
} else {
|
||||
note.data = serializer.decode(_textController.text);
|
||||
}
|
||||
if (!note.isEmpty()) {
|
||||
newNote ? stateContainer.addNote(note) : stateContainer.updateNote(note);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class NoteMarkdownEditor extends StatelessWidget {
|
||||
final TextEditingController textController;
|
||||
final bool useMonospace;
|
||||
|
||||
NoteMarkdownEditor(this.textController, this.useMonospace);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var style = Theme.of(context).textTheme.subhead;
|
||||
if (useMonospace) {
|
||||
style = style.copyWith(fontFamily: "Roboto Mono");
|
||||
}
|
||||
|
||||
return TextField(
|
||||
autofocus: false,
|
||||
autocorrect: false,
|
||||
keyboardType: TextInputType.multiline,
|
||||
maxLines: null,
|
||||
style: style,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Write here',
|
||||
border: InputBorder.none,
|
||||
isDense: true,
|
||||
),
|
||||
controller: textController,
|
||||
textCapitalization: TextCapitalization.sentences,
|
||||
scrollPadding: const EdgeInsets.all(0.0),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class NoteTitleEditor extends StatelessWidget {
|
||||
final TextEditingController textController;
|
||||
|
||||
NoteTitleEditor(this.textController);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var style = Theme.of(context).textTheme.title;
|
||||
|
||||
return TextField(
|
||||
autofocus: false,
|
||||
keyboardType: TextInputType.text,
|
||||
maxLines: 1,
|
||||
style: style,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Title',
|
||||
border: InputBorder.none,
|
||||
isDense: true,
|
||||
),
|
||||
controller: textController,
|
||||
textCapitalization: TextCapitalization.sentences,
|
||||
);
|
||||
}
|
||||
}
|
@ -2,8 +2,7 @@ import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:gitjournal/core/note.dart';
|
||||
import 'package:gitjournal/core/notes_folder.dart';
|
||||
import 'package:gitjournal/screens/journal_editor.dart';
|
||||
import 'package:gitjournal/screens/journal_browsing.dart';
|
||||
import 'package:gitjournal/screens/note_editor.dart';
|
||||
import 'package:gitjournal/state_container.dart';
|
||||
import 'package:gitjournal/widgets/app_drawer.dart';
|
||||
import 'package:gitjournal/widgets/app_bar_menu_button.dart';
|
||||
@ -33,11 +32,9 @@ class JournalListingScreen extends StatelessWidget {
|
||||
Widget journalList = JournalList(
|
||||
notes: allNotes,
|
||||
noteSelectedFunction: (noteIndex) {
|
||||
var note = allNotes[noteIndex];
|
||||
var route = MaterialPageRoute(
|
||||
builder: (context) => JournalBrowsingScreen(
|
||||
notes: allNotes,
|
||||
noteIndex: noteIndex,
|
||||
),
|
||||
builder: (context) => NoteEditor.fromNote(note),
|
||||
);
|
||||
Navigator.of(context).push(route);
|
||||
},
|
||||
@ -81,7 +78,7 @@ class JournalListingScreen extends StatelessWidget {
|
||||
|
||||
void _newPost(BuildContext context) {
|
||||
var route = MaterialPageRoute(
|
||||
builder: (context) => JournalEditor.newNote(notesFolder));
|
||||
builder: (context) => NoteEditor.newNote(notesFolder));
|
||||
Navigator.of(context).push(route);
|
||||
}
|
||||
}
|
||||
@ -146,11 +143,9 @@ class NoteSearch extends SearchDelegate<Note> {
|
||||
Widget journalList = JournalList(
|
||||
notes: filteredNotes,
|
||||
noteSelectedFunction: (noteIndex) {
|
||||
var note = filteredNotes[noteIndex];
|
||||
var route = MaterialPageRoute(
|
||||
builder: (context) => JournalBrowsingScreen(
|
||||
notes: filteredNotes,
|
||||
noteIndex: noteIndex,
|
||||
),
|
||||
builder: (context) => NoteEditor.fromNote(note),
|
||||
);
|
||||
Navigator.of(context).push(route);
|
||||
},
|
||||
|
230
lib/screens/note_editor.dart
Normal file
230
lib/screens/note_editor.dart
Normal file
@ -0,0 +1,230 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:fimber/fimber.dart';
|
||||
|
||||
import 'package:gitjournal/core/note.dart';
|
||||
import 'package:gitjournal/core/notes_folder.dart';
|
||||
import 'package:gitjournal/editors/markdown_editor.dart';
|
||||
import 'package:gitjournal/editors/raw_editor.dart';
|
||||
import 'package:gitjournal/state_container.dart';
|
||||
import 'package:gitjournal/core/note_data_serializers.dart';
|
||||
import 'package:gitjournal/utils.dart';
|
||||
import 'package:gitjournal/widgets/rename_dialog.dart';
|
||||
|
||||
enum NoteEditorDropDownChoices { Discard, SwitchEditor }
|
||||
|
||||
class NoteEditor extends StatefulWidget {
|
||||
final Note note;
|
||||
final NotesFolder notesFolder;
|
||||
|
||||
NoteEditor.fromNote(this.note) : notesFolder = null;
|
||||
NoteEditor.newNote(this.notesFolder) : note = null;
|
||||
|
||||
@override
|
||||
NoteEditorState createState() {
|
||||
if (note == null) {
|
||||
return NoteEditorState.newNote(notesFolder);
|
||||
} else {
|
||||
return NoteEditorState.fromNote(note);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum EditorType { Markdown, Raw }
|
||||
|
||||
class NoteEditorState extends State<NoteEditor> {
|
||||
Note note;
|
||||
EditorType editorType = EditorType.Markdown;
|
||||
String noteSerialized = "";
|
||||
|
||||
final _rawEditorKey = GlobalKey<RawEditorState>();
|
||||
final _markdownEditorKey = GlobalKey<MarkdownEditorState>();
|
||||
|
||||
bool get _isNewNote {
|
||||
return widget.note == null;
|
||||
}
|
||||
|
||||
NoteEditorState.newNote(NotesFolder folder) {
|
||||
note = Note.newNote(folder);
|
||||
}
|
||||
|
||||
NoteEditorState.fromNote(this.note) {
|
||||
var serializer = MarkdownYAMLSerializer();
|
||||
noteSerialized = serializer.encode(note.data);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return WillPopScope(
|
||||
onWillPop: () async {
|
||||
_saveNote(_getNoteFromEditor());
|
||||
return true;
|
||||
},
|
||||
child: _getEditor(),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _getEditor() {
|
||||
switch (editorType) {
|
||||
case EditorType.Markdown:
|
||||
return MarkdownEditor(
|
||||
key: _markdownEditorKey,
|
||||
note: note,
|
||||
noteDeletionSelected: _noteDeletionSelected,
|
||||
noteEditorChooserSelected: _noteEditorChooserSelected,
|
||||
exitEditorSelected: _exitEditorSelected,
|
||||
renameNoteSelected: _renameNoteSelected,
|
||||
autofocusOnEditor: _isNewNote,
|
||||
);
|
||||
case EditorType.Raw:
|
||||
return RawEditor(
|
||||
key: _rawEditorKey,
|
||||
note: note,
|
||||
noteDeletionSelected: _noteDeletionSelected,
|
||||
noteEditorChooserSelected: _noteEditorChooserSelected,
|
||||
exitEditorSelected: _exitEditorSelected,
|
||||
renameNoteSelected: _renameNoteSelected,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
void _noteEditorChooserSelected(Note _note) async {
|
||||
var newEditorType = await showDialog<EditorType>(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
var children = <Widget>[
|
||||
RadioListTile<EditorType>(
|
||||
title: const Text("Markdown Editor"),
|
||||
value: EditorType.Markdown,
|
||||
groupValue: editorType,
|
||||
onChanged: (EditorType et) => Navigator.of(context).pop(et),
|
||||
),
|
||||
RadioListTile<EditorType>(
|
||||
title: const Text("Raw Editor"),
|
||||
value: EditorType.Raw,
|
||||
groupValue: editorType,
|
||||
onChanged: (EditorType et) => Navigator.of(context).pop(et),
|
||||
),
|
||||
];
|
||||
|
||||
return AlertDialog(
|
||||
title: const Text("Choose Editor"),
|
||||
content: Column(
|
||||
children: children,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
if (newEditorType != null) {
|
||||
setState(() {
|
||||
note = _note;
|
||||
editorType = newEditorType;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _exitEditorSelected(Note note) {
|
||||
_saveNote(note);
|
||||
Navigator.pop(context);
|
||||
}
|
||||
|
||||
void _renameNoteSelected(Note _note) async {
|
||||
var fileName = await showDialog(
|
||||
context: context,
|
||||
builder: (_) => RenameDialog(
|
||||
oldPath: note.filePath,
|
||||
inputDecoration: 'File Name',
|
||||
dialogTitle: "Rename File",
|
||||
),
|
||||
);
|
||||
if (fileName is String) {
|
||||
if (_isNewNote) {
|
||||
setState(() {
|
||||
note = _note;
|
||||
note.rename(fileName);
|
||||
});
|
||||
}
|
||||
final container = StateContainer.of(context);
|
||||
container.renameNote(note, fileName);
|
||||
}
|
||||
}
|
||||
|
||||
void _noteDeletionSelected(Note note) {
|
||||
if (_isNewNote && !_noteModified(note)) {
|
||||
Navigator.pop(context);
|
||||
return;
|
||||
}
|
||||
|
||||
showDialog(context: context, builder: _buildAlertDialog);
|
||||
}
|
||||
|
||||
void _deleteNote(Note note) {
|
||||
if (_isNewNote) {
|
||||
return;
|
||||
}
|
||||
|
||||
final stateContainer = StateContainer.of(context);
|
||||
stateContainer.removeNote(note);
|
||||
}
|
||||
|
||||
Widget _buildAlertDialog(BuildContext context) {
|
||||
var title = "Do you want to delete this note?";
|
||||
var editText = "Keep Writing";
|
||||
var discardText = "Discard";
|
||||
|
||||
return AlertDialog(
|
||||
title: Text(title),
|
||||
actions: <Widget>[
|
||||
FlatButton(
|
||||
onPressed: () => Navigator.of(context).pop(false),
|
||||
child: Text(editText),
|
||||
),
|
||||
FlatButton(
|
||||
onPressed: () {
|
||||
_deleteNote(note);
|
||||
|
||||
Navigator.pop(context); // Alert box
|
||||
Navigator.pop(context); // Note Editor
|
||||
|
||||
if (!_isNewNote) {
|
||||
Fimber.d("Showing an undo snackbar");
|
||||
|
||||
final stateContainer = StateContainer.of(context);
|
||||
showUndoDeleteSnackbar(context, stateContainer, note);
|
||||
}
|
||||
},
|
||||
child: Text(discardText),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
bool _noteModified(Note note) {
|
||||
if (_isNewNote) {
|
||||
return note.title.isNotEmpty || note.body.isNotEmpty;
|
||||
}
|
||||
var serializer = MarkdownYAMLSerializer();
|
||||
var finalNoteSerialized = serializer.encode(note.data);
|
||||
return finalNoteSerialized != noteSerialized;
|
||||
}
|
||||
|
||||
void _saveNote(Note note) {
|
||||
if (!_noteModified(note)) return;
|
||||
|
||||
print("Note modified - saving");
|
||||
final stateContainer = StateContainer.of(context);
|
||||
_isNewNote ? stateContainer.addNote(note) : stateContainer.updateNote(note);
|
||||
}
|
||||
|
||||
Note _getNoteFromEditor() {
|
||||
switch (editorType) {
|
||||
case EditorType.Markdown:
|
||||
return _markdownEditorKey.currentState.getNote();
|
||||
case EditorType.Raw:
|
||||
return _rawEditorKey.currentState.getNote();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
@ -151,11 +151,6 @@ class StateContainerState extends State<StateContainer> {
|
||||
}
|
||||
|
||||
void renameNote(Note note, String newFileName) async {
|
||||
// Do not let the user rename it to a non-markdown file
|
||||
if (!newFileName.toLowerCase().endsWith('.md')) {
|
||||
newFileName += '.md';
|
||||
}
|
||||
|
||||
var oldNotePath = note.filePath;
|
||||
note.rename(newFileName);
|
||||
|
||||
@ -181,8 +176,8 @@ class StateContainerState extends State<StateContainer> {
|
||||
});
|
||||
}
|
||||
|
||||
void undoRemoveNote(Note note, int index) {
|
||||
note.parent.insert(index, note);
|
||||
void undoRemoveNote(Note note) {
|
||||
note.parent.insert(0, note);
|
||||
_gitRepo.resetLastCommit().then((NoteRepoResult _) {
|
||||
syncNotes();
|
||||
});
|
||||
|
@ -26,7 +26,6 @@ void showUndoDeleteSnackbar(
|
||||
BuildContext context,
|
||||
StateContainerState stateContainer,
|
||||
Note deletedNote,
|
||||
int deletedNoteIndex,
|
||||
) {
|
||||
var theme = Theme.of(context);
|
||||
|
||||
@ -40,7 +39,7 @@ void showUndoDeleteSnackbar(
|
||||
),
|
||||
onPressed: () {
|
||||
Fimber.d("Undoing delete");
|
||||
stateContainer.undoRemoveNote(deletedNote, deletedNoteIndex);
|
||||
stateContainer.undoRemoveNote(deletedNote);
|
||||
},
|
||||
),
|
||||
).show(context);
|
||||
|
@ -64,7 +64,7 @@ class JournalList extends StatelessWidget {
|
||||
final stateContainer = StateContainer.of(context);
|
||||
stateContainer.removeNote(note);
|
||||
|
||||
showUndoDeleteSnackbar(context, stateContainer, note, i);
|
||||
showUndoDeleteSnackbar(context, stateContainer, note);
|
||||
},
|
||||
);
|
||||
},
|
||||
|
@ -47,7 +47,7 @@ class _RenameDialogState extends State<RenameDialog> {
|
||||
FileSystemEntityType.notFound) {
|
||||
return 'Already Exists';
|
||||
}
|
||||
return "";
|
||||
return null;
|
||||
},
|
||||
autofocus: true,
|
||||
keyboardType: TextInputType.text,
|
||||
|
Reference in New Issue
Block a user