From 9859aa6106891b096288fb7392ab9864e7924266 Mon Sep 17 00:00:00 2001 From: Vishesh Handa Date: Tue, 18 Aug 2020 10:47:26 +0200 Subject: [PATCH] Allow custom metadata to be specifiec for any note This way we can add 'draft: true' to all new notes. Fixes #168 --- assets/langs/en.yaml | 4 + lib/app.dart | 10 ++ lib/core/md_yaml_doc_codec.dart | 9 +- lib/core/note.dart | 12 +++ lib/core/note_serializer.dart | 26 +++++ lib/features.dart | 9 ++ lib/screens/folder_view.dart | 12 ++- lib/screens/settings_note_metadata.dart | 129 +++++++++++++++++++++++- lib/settings.dart | 5 + test/note_serializer_test.dart | 24 +++++ 10 files changed, 234 insertions(+), 6 deletions(-) diff --git a/assets/langs/en.yaml b/assets/langs/en.yaml index ea81c910..ec04c0e7 100644 --- a/assets/langs/en.yaml +++ b/assets/langs/en.yaml @@ -57,6 +57,9 @@ settings: exampleBody: I think they might be evil. Even more evil than penguins. exampleTag1: Birds exampleTag2: Evil + customMetaData: + title: Custom MetaData + invalid: Invalid YAML privacy: Privacy Policy terms: Terms and Conditions experimental: @@ -218,6 +221,7 @@ feature: allNotesView: Add a screen to show "All Notes" basicSearch: Basic Search customSSHKeys: Provide your own SSH Keys + customMetaData: Add Custom Metadata to new Notes feature_timeline: title: Feature Timeline diff --git a/lib/app.dart b/lib/app.dart index 22e2ce3a..7d2b80f3 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -20,6 +20,7 @@ import 'package:shared_preferences/shared_preferences.dart'; import 'package:gitjournal/analytics.dart'; import 'package:gitjournal/appstate.dart'; +import 'package:gitjournal/core/md_yaml_doc_codec.dart'; import 'package:gitjournal/iap.dart'; import 'package:gitjournal/screens/filesystem_screen.dart'; import 'package:gitjournal/screens/folder_listing.dart'; @@ -381,11 +382,20 @@ class _JournalAppState extends State { Log.d("sharedText: $sharedText"); Log.d("sharedImages: $sharedImages"); + var extraProps = {}; + if (settings.customMetaData.isNotEmpty) { + var map = MarkdownYAMLCodec.parseYamlText(settings.customMetaData); + map.forEach((key, val) { + extraProps[key] = val; + }); + } + return NoteEditor.newNote( getFolderForEditor(settings, rootFolder, et), et, existingText: sharedText, existingImages: sharedImages, + newNoteExtraProps: extraProps, ); } diff --git a/lib/core/md_yaml_doc_codec.dart b/lib/core/md_yaml_doc_codec.dart index 1955ae5b..3aac691d 100644 --- a/lib/core/md_yaml_doc_codec.dart +++ b/lib/core/md_yaml_doc_codec.dart @@ -32,7 +32,7 @@ class MarkdownYAMLCodec { if (str.endsWith(endYamlStrWithoutLineEding)) { var yamlText = str.substring(4, str.length - endYamlStrWithoutLineEding.length); - var map = _parseYamlText(yamlText); + var map = parseYamlText(yamlText); return MdYamlDoc("", map); } @@ -40,7 +40,7 @@ class MarkdownYAMLCodec { } var yamlText = str.substring(4, endYamlPos); - var map = _parseYamlText(yamlText); + var map = parseYamlText(yamlText); var body = ""; var bodyBeginingPos = endYamlPos + endYamlStr.length; @@ -59,7 +59,7 @@ class MarkdownYAMLCodec { return MdYamlDoc(str, LinkedHashMap()); } - LinkedHashMap _parseYamlText(String yamlText) { + static LinkedHashMap parseYamlText(String yamlText) { LinkedHashMap map = LinkedHashMap(); if (yamlText.isEmpty) { return map; @@ -67,6 +67,9 @@ class MarkdownYAMLCodec { try { var yamlMap = loadYaml(yamlText); + if (yamlMap is! Map) { + return map; + } yamlMap.forEach((key, value) { map[key] = value; }); diff --git a/lib/core/note.dart b/lib/core/note.dart index c6850fe3..c2747bce 100644 --- a/lib/core/note.dart +++ b/lib/core/note.dart @@ -46,6 +46,7 @@ class Note with NotesNotifier { String _body = ""; NoteType _type = NoteType.Unknown; Set _tags = {}; + Map _extraProps = {}; NoteFileFormat _fileFormat; @@ -185,6 +186,17 @@ class Note with NotesNotifier { _notifyModified(); } + Map get extraProps { + return _extraProps; + } + + set extraProps(Map props) { + if (!canHaveMetadata) return; + + _extraProps = props; + _notifyModified(); + } + bool get canHaveMetadata { if (_fileFormat == NoteFileFormat.Txt) { return false; diff --git a/lib/core/note_serializer.dart b/lib/core/note_serializer.dart index c18c974a..c0b9c318 100644 --- a/lib/core/note_serializer.dart +++ b/lib/core/note_serializer.dart @@ -84,10 +84,16 @@ class NoteSerializer implements NoteSerializerInterface { } else { data.props[settings.tagsKey] = note.tags.toList(); } + + note.extraProps.forEach((key, value) { + data.props[key] = value; + }); } @override void decode(MdYamlDoc data, Note note) { + var propsUsed = {}; + var modifiedKeyOptions = [ "modified", "mod", @@ -101,6 +107,8 @@ class NoteSerializer implements NoteSerializerInterface { if (val != null) { note.modified = parseDateTime(val.toString()); settings.modifiedKey = possibleKey; + + propsUsed.add(possibleKey); break; } } @@ -116,6 +124,8 @@ class NoteSerializer implements NoteSerializerInterface { if (val != null) { note.created = parseDateTime(val.toString()); settings.createdKey = possibleKey; + + propsUsed.add(possibleKey); break; } } @@ -126,6 +136,8 @@ class NoteSerializer implements NoteSerializerInterface { if (data.props.containsKey(settings.titleKey)) { var title = data.props[settings.titleKey]?.toString() ?? ""; note.title = emojiParser.emojify(title); + + propsUsed.add(settings.titleKey); } else { var startsWithH1 = false; for (var line in LineSplitter.split(note.body)) { @@ -162,6 +174,9 @@ class NoteSerializer implements NoteSerializerInterface { note.type = NoteType.Unknown; break; } + if (type != null) { + propsUsed.add(settings.typeKey); + } try { var tagKeyOptions = [ @@ -180,11 +195,22 @@ class NoteSerializer implements NoteSerializerInterface { } settings.tagsKey = possibleKey; + propsUsed.add(settings.tagsKey); break; } } } catch (e) { Log.e("Note Decoding Failed: $e"); } + + // Extra Props + note.extraProps = {}; + data.props.forEach((key, val) { + if (propsUsed.contains(key)) { + return; + } + + note.extraProps[key] = val; + }); } } diff --git a/lib/features.dart b/lib/features.dart index 53e96364..44ef9e35 100644 --- a/lib/features.dart +++ b/lib/features.dart @@ -34,6 +34,7 @@ class Features { Feature.metaDataTitle, Feature.yamlCreatedKey, Feature.yamlTagsKey, + Feature.customMetaData, ]; } @@ -285,6 +286,14 @@ class Feature { "", true, ); + + static final customMetaData = Feature( + "customMetaData", + DateTime(2020, 08, 18), + tr("feature.customMetaData"), + "", + true, + ); } // Feature Adding checklist diff --git a/lib/screens/folder_view.dart b/lib/screens/folder_view.dart index 72da7a6e..12227d5a 100644 --- a/lib/screens/folder_view.dart +++ b/lib/screens/folder_view.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:git_bindings/git_bindings.dart'; import 'package:provider/provider.dart'; +import 'package:gitjournal/core/md_yaml_doc_codec.dart'; import 'package:gitjournal/core/note.dart'; import 'package:gitjournal/core/notes_folder.dart'; import 'package:gitjournal/core/notes_folder_fs.dart'; @@ -176,11 +177,20 @@ class _FolderViewState extends State { var routeType = SettingsEditorType.fromEditorType(editorType).toInternalString(); + + var settings = Provider.of(context); + var extraProps = Map.from(widget.newNoteExtraProps); + if (settings.customMetaData.isNotEmpty) { + var map = MarkdownYAMLCodec.parseYamlText(settings.customMetaData); + map.forEach((key, val) { + extraProps[key] = val; + }); + } var route = MaterialPageRoute( builder: (context) => NoteEditor.newNote( fsFolder, editorType, - newNoteExtraProps: widget.newNoteExtraProps, + newNoteExtraProps: extraProps, ), settings: RouteSettings(name: '/newNote/$routeType'), ); diff --git a/lib/screens/settings_note_metadata.dart b/lib/screens/settings_note_metadata.dart index be34c772..21460bcb 100644 --- a/lib/screens/settings_note_metadata.dart +++ b/lib/screens/settings_note_metadata.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:easy_localization/easy_localization.dart'; +import 'package:function_types/function_types.dart'; import 'package:provider/provider.dart'; import 'package:gitjournal/core/md_yaml_doc.dart'; @@ -23,6 +24,17 @@ class NoteMetadataSettingsScreen extends StatefulWidget { class _NoteMetadataSettingsScreenState extends State { + DateTime created; + DateTime modified; + + @override + void initState() { + super.initState(); + + created = DateTime.now().add(const Duration(days: -1)); + modified = DateTime.now(); + } + @override Widget build(BuildContext context) { var textTheme = Theme.of(context).textTheme; @@ -32,13 +44,21 @@ class _NoteMetadataSettingsScreenState var note = Note(parent, "fileName.md"); note.title = tr("settings.noteMetaData.exampleTitle"); note.body = tr("settings.noteMetaData.exampleBody"); - note.created = DateTime.now().add(const Duration(days: -1)); - note.modified = DateTime.now(); + note.created = created; + note.modified = modified; note.tags = { tr("settings.noteMetaData.exampleTag1"), tr("settings.noteMetaData.exampleTag2"), }; + if (settings.customMetaData != "") { + var customMetaDataMap = + MarkdownYAMLCodec.parseYamlText(settings.customMetaData); + if (customMetaDataMap.isNotEmpty) { + note.extraProps = customMetaDataMap; + } + } + var body = Column( children: [ Padding( @@ -144,6 +164,18 @@ class _NoteMetadataSettingsScreenState }, ), ), + ProOverlay( + feature: Feature.customMetaData, + child: CustomMetDataTile( + value: settings.customMetaData, + onChange: (String newVal) { + setState(() { + settings.customMetaData = newVal; + settings.save(); + }); + }, + ), + ), ], ); @@ -300,3 +332,96 @@ class TagsWidget extends StatelessWidget { ); } } + +class CustomMetDataTile extends StatefulWidget { + final String value; + final Func1 onChange; + + CustomMetDataTile({@required this.value, @required this.onChange}); + + @override + _CustomMetDataTileState createState() => _CustomMetDataTileState(); +} + +class _CustomMetDataTileState extends State { + TextEditingController _textController; + + @override + void initState() { + _textController = TextEditingController(text: widget.value); + + super.initState(); + } + + @override + Widget build(BuildContext context) { + /* + var settings = Provider.of(context); + settings.customMetaData = "draft: true"; + settings.save(); + */ + + return ListTile( + title: Text(tr("settings.noteMetaData.customMetaData.title")), + subtitle: Text(widget.value), + onTap: () async { + var val = + await showDialog(context: context, builder: _buildDialog); + + val ??= ""; + if (val != widget.value) { + widget.onChange(val); + } + }, + ); + } + + Widget _buildDialog(BuildContext context) { + var form = Form( + child: TextFormField( + validator: (value) { + value = value.trim(); + if (value.isEmpty) { + return ""; + } + + var map = MarkdownYAMLCodec.parseYamlText(value); + if (map == null || map.isEmpty) { + return tr("settings.noteMetaData.customMetaData.invalid"); + } + return ""; + }, + autofocus: true, + keyboardType: TextInputType.multiline, + textCapitalization: TextCapitalization.words, + controller: _textController, + autovalidate: true, + maxLines: null, + minLines: null, + ), + ); + + return AlertDialog( + title: Text(tr("settings.noteMetaData.customMetaData.title")), + actions: [ + FlatButton( + onPressed: () => Navigator.of(context).pop(widget.value), + child: Text(tr("settings.cancel")), + ), + FlatButton( + onPressed: () { + var text = _textController.text.trim(); + var map = MarkdownYAMLCodec.parseYamlText(text); + if (map == null || map.isEmpty) { + return Navigator.of(context).pop(); + } + + return Navigator.of(context).pop(text); + }, + child: Text(tr("settings.ok")), + ), + ], + content: form, + ); + } +} diff --git a/lib/settings.dart b/lib/settings.dart index aa4cc07d..da561876 100644 --- a/lib/settings.dart +++ b/lib/settings.dart @@ -28,6 +28,7 @@ class Settings extends ChangeNotifier { String yamlModifiedKey = "modified"; String yamlCreatedKey = "created"; String yamlTagsKey = "tags"; + String customMetaData = ""; bool yamlHeaderEnabled = true; String defaultNewNoteFolderSpec = ""; @@ -82,6 +83,7 @@ class Settings extends ChangeNotifier { yamlModifiedKey = pref.getString("yamlModifiedKey") ?? yamlModifiedKey; yamlCreatedKey = pref.getString("yamlCreatedKey") ?? yamlCreatedKey; yamlTagsKey = pref.getString("yamlTagsKey") ?? yamlTagsKey; + customMetaData = pref.getString("customMetaData") ?? customMetaData; yamlHeaderEnabled = pref.getBool("yamlHeaderEnabled") ?? yamlHeaderEnabled; defaultNewNoteFolderSpec = @@ -165,6 +167,8 @@ class Settings extends ChangeNotifier { _setString( pref, "yamlCreatedKey", yamlCreatedKey, defaultSet.yamlCreatedKey); _setString(pref, "yamlTagsKey", yamlTagsKey, defaultSet.yamlTagsKey); + _setString( + pref, "customMetaData", customMetaData, defaultSet.customMetaData); _setBool(pref, "yamlHeaderEnabled", yamlHeaderEnabled, defaultSet.yamlHeaderEnabled); _setString(pref, "defaultNewNoteFolderSpec", defaultNewNoteFolderSpec, @@ -259,6 +263,7 @@ class Settings extends ChangeNotifier { "yamlModifiedKey": yamlModifiedKey, "yamlCreatedKey": yamlCreatedKey, "yamlTagsKey": yamlTagsKey, + "customMetaData": customMetaData, "yamlHeaderEnabled": yamlHeaderEnabled.toString(), "defaultNewNoteFolderSpec": defaultNewNoteFolderSpec, "journalEditordefaultNewNoteFolderSpec": diff --git a/test/note_serializer_test.dart b/test/note_serializer_test.dart index cfee0e64..abea0cf9 100644 --- a/test/note_serializer_test.dart +++ b/test/note_serializer_test.dart @@ -98,5 +98,29 @@ void main() { expect(doc.body, "# Why not :coffee:?\n\nI :heart: you"); expect(doc.props.length, 0); }); + + test('Test Note ExtraProps', () { + var props = LinkedHashMap.from({ + "title": "Why not?", + "draft": true, + }); + var doc = MdYamlDoc("body", props); + + var serializer = NoteSerializer.raw(); + serializer.settings.saveTitleAsH1 = false; + + var note = Note(parent, "file-path-not-important"); + serializer.decode(doc, note); + + expect(note.body, "body"); + expect(note.title, "Why not?"); + expect(note.extraProps, {"draft": true}); + + serializer.encode(note, doc); + expect(doc.body, "body"); + expect(doc.props.length, 2); + expect(doc.props['title'], 'Why not?'); + expect(doc.props['draft'], true); + }); }); }