Allow custom metadata to be specifiec for any note

This way we can add 'draft: true' to all new notes.

Fixes #168
This commit is contained in:
Vishesh Handa
2020-08-18 10:47:26 +02:00
parent bc68b7e4ee
commit 9859aa6106
10 changed files with 234 additions and 6 deletions

View File

@ -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

View File

@ -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<JournalApp> {
Log.d("sharedText: $sharedText");
Log.d("sharedImages: $sharedImages");
var extraProps = <String, dynamic>{};
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,
);
}

View File

@ -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<String, dynamic>());
}
LinkedHashMap<String, dynamic> _parseYamlText(String yamlText) {
static LinkedHashMap<String, dynamic> parseYamlText(String yamlText) {
LinkedHashMap<String, dynamic> map = LinkedHashMap<String, dynamic>();
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;
});

View File

@ -46,6 +46,7 @@ class Note with NotesNotifier {
String _body = "";
NoteType _type = NoteType.Unknown;
Set<String> _tags = {};
Map<String, dynamic> _extraProps = {};
NoteFileFormat _fileFormat;
@ -185,6 +186,17 @@ class Note with NotesNotifier {
_notifyModified();
}
Map<String, dynamic> get extraProps {
return _extraProps;
}
set extraProps(Map<String, dynamic> props) {
if (!canHaveMetadata) return;
_extraProps = props;
_notifyModified();
}
bool get canHaveMetadata {
if (_fileFormat == NoteFileFormat.Txt) {
return false;

View File

@ -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 = <String>{};
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;
});
}
}

View File

@ -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

View File

@ -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<FolderView> {
var routeType =
SettingsEditorType.fromEditorType(editorType).toInternalString();
var settings = Provider.of<Settings>(context);
var extraProps = Map<String, dynamic>.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'),
);

View File

@ -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<NoteMetadataSettingsScreen> {
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: <Widget>[
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<String, void> onChange;
CustomMetDataTile({@required this.value, @required this.onChange});
@override
_CustomMetDataTileState createState() => _CustomMetDataTileState();
}
class _CustomMetDataTileState extends State<CustomMetDataTile> {
TextEditingController _textController;
@override
void initState() {
_textController = TextEditingController(text: widget.value);
super.initState();
}
@override
Widget build(BuildContext context) {
/*
var settings = Provider.of<Settings>(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<String>(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: <Widget>[
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,
);
}
}

View File

@ -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":

View File

@ -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<String, dynamic>.from(<String, dynamic>{
"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, <String, dynamic>{"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);
});
});
}