mirror of
https://github.com/GitJournal/GitJournal.git
synced 2025-06-30 19:36:25 +08:00
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:
@ -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
|
||||
|
10
lib/app.dart
10
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<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,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
});
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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'),
|
||||
);
|
||||
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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":
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
Reference in New Issue
Block a user