mirror of
https://github.com/GitJournal/GitJournal.git
synced 2025-08-06 15:21:21 +08:00

Fixes #78 This is probably the largest commit that I have ever made. From now on - every File always has an mtime and ctime which is fetched from git. Notes can optionally override that time by providing yaml metadata. Additionally the 'filePath' and 'folderPath' is now relative to the repoPath instead of being the full path. This will slow down GitJournal like crazy as all the mtimes and ctime still need to be cached. For my test repo it takes about 23 seconds for GitJournal to become responsive.
352 lines
9.3 KiB
Dart
352 lines
9.3 KiB
Dart
/*
|
|
* SPDX-FileCopyrightText: 2019-2021 Vishesh Handa <me@vhanda.in>
|
|
*
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
import 'dart:convert';
|
|
|
|
import 'package:flutter_emoji/flutter_emoji.dart';
|
|
import 'package:yaml/yaml.dart';
|
|
|
|
import 'package:gitjournal/core/folder/notes_folder.dart';
|
|
import 'package:gitjournal/core/folder/notes_folder_fs.dart';
|
|
import 'package:gitjournal/logger/logger.dart';
|
|
import 'package:gitjournal/settings/settings.dart';
|
|
import 'package:gitjournal/utils/datetime.dart';
|
|
import 'file/file.dart';
|
|
import 'md_yaml_doc.dart';
|
|
import 'note.dart';
|
|
|
|
abstract class NoteSerializerInterface {
|
|
void encode(Note note, MdYamlDoc data);
|
|
Note decode({
|
|
required MdYamlDoc data,
|
|
required NotesFolderFS parent,
|
|
required File file,
|
|
});
|
|
}
|
|
|
|
var emojiParser = EmojiParser();
|
|
|
|
enum DateFormat {
|
|
Iso8601,
|
|
UnixTimeStamp,
|
|
None,
|
|
}
|
|
|
|
class NoteSerializationSettings {
|
|
String modifiedKey = "modified";
|
|
String createdKey = "created";
|
|
String titleKey = "title";
|
|
String typeKey = "type";
|
|
String tagsKey = "tags";
|
|
|
|
bool tagsInString = false;
|
|
bool tagsHaveHash = false;
|
|
|
|
SettingsTitle titleSettings = SettingsTitle.Default;
|
|
|
|
var modifiedFormat = DateFormat.Iso8601;
|
|
var createdFormat = DateFormat.Iso8601;
|
|
|
|
var emojify = false;
|
|
|
|
NoteSerializationSettings.fromConfig(NotesFolderConfig config) {
|
|
modifiedKey = config.yamlModifiedKey;
|
|
createdKey = config.yamlCreatedKey;
|
|
tagsKey = config.yamlTagsKey;
|
|
titleSettings = config.titleSettings;
|
|
|
|
// FIXME: modified / created format!
|
|
}
|
|
NoteSerializationSettings();
|
|
|
|
NoteSerializationSettings clone() {
|
|
var s = NoteSerializationSettings();
|
|
s.createdKey = createdKey;
|
|
s.createdFormat = createdFormat;
|
|
s.modifiedKey = modifiedKey;
|
|
s.modifiedFormat = modifiedFormat;
|
|
s.titleKey = titleKey;
|
|
s.typeKey = typeKey;
|
|
s.tagsKey = tagsKey;
|
|
s.tagsInString = tagsInString;
|
|
s.tagsHaveHash = tagsHaveHash;
|
|
s.titleSettings = titleSettings;
|
|
s.emojify = emojify;
|
|
return s;
|
|
}
|
|
}
|
|
|
|
// Rename to MarkdownYamlNoteSerializer
|
|
class NoteSerializer implements NoteSerializerInterface {
|
|
var settings = NoteSerializationSettings();
|
|
|
|
NoteSerializer.fromConfig(this.settings);
|
|
NoteSerializer.raw();
|
|
|
|
@override
|
|
void encode(Note note, MdYamlDoc data) {
|
|
data.body = settings.emojify ? emojiParser.unemojify(note.body) : note.body;
|
|
|
|
switch (settings.createdFormat) {
|
|
case DateFormat.Iso8601:
|
|
data.props[settings.createdKey] = toIso8601WithTimezone(note.created);
|
|
break;
|
|
case DateFormat.UnixTimeStamp:
|
|
data.props[settings.createdKey] = toUnixTimeStamp(note.created);
|
|
break;
|
|
case DateFormat.None:
|
|
data.props.remove(settings.createdKey);
|
|
break;
|
|
}
|
|
|
|
switch (settings.modifiedFormat) {
|
|
case DateFormat.Iso8601:
|
|
data.props[settings.modifiedKey] = toIso8601WithTimezone(note.modified);
|
|
break;
|
|
case DateFormat.UnixTimeStamp:
|
|
data.props[settings.modifiedKey] = toUnixTimeStamp(note.modified);
|
|
break;
|
|
case DateFormat.None:
|
|
data.props.remove(settings.modifiedKey);
|
|
break;
|
|
}
|
|
|
|
if (note.title.isNotEmpty) {
|
|
var title = settings.emojify
|
|
? emojiParser.unemojify(note.title.trim())
|
|
: note.title.trim();
|
|
if (settings.titleSettings == SettingsTitle.InH1) {
|
|
if (title.isNotEmpty) {
|
|
data.body = '# $title\n\n${data.body}';
|
|
data.props.remove(settings.titleKey);
|
|
}
|
|
} else {
|
|
if (title.isNotEmpty) {
|
|
data.props[settings.titleKey] = title;
|
|
} else {
|
|
data.props.remove(settings.titleKey);
|
|
}
|
|
}
|
|
} else {
|
|
data.props.remove(settings.titleKey);
|
|
}
|
|
|
|
if (note.type != NoteType.Unknown) {
|
|
var type = note.type.toString().substring(9); // Remove "NoteType."
|
|
data.props[settings.typeKey] = type;
|
|
} else {
|
|
data.props.remove(settings.typeKey);
|
|
}
|
|
|
|
if (note.tags.isEmpty) {
|
|
data.props.remove(settings.tagsKey);
|
|
} else {
|
|
data.props[settings.tagsKey] = note.tags.toList();
|
|
if (settings.tagsInString) {
|
|
var tags = note.tags;
|
|
if (settings.tagsHaveHash) {
|
|
tags = tags.map((e) => '#$e').toSet();
|
|
}
|
|
data.props[settings.tagsKey] = tags.join(' ');
|
|
}
|
|
}
|
|
|
|
note.extraProps.forEach((key, value) {
|
|
data.props[key] = value;
|
|
});
|
|
}
|
|
|
|
static Note decodeNote({
|
|
required MdYamlDoc data,
|
|
required NotesFolderFS parent,
|
|
required File file,
|
|
required NoteSerializationSettings settings,
|
|
}) {
|
|
var serializer = NoteSerializer.fromConfig(settings.clone());
|
|
return serializer.decode(data: data, parent: parent, file: file);
|
|
}
|
|
|
|
@override
|
|
Note decode({
|
|
required MdYamlDoc data,
|
|
required NotesFolderFS parent,
|
|
required File file,
|
|
}) {
|
|
assert(file.filePath.isNotEmpty);
|
|
|
|
var propsUsed = <String>{};
|
|
|
|
DateTime? modified;
|
|
var modifiedKeyOptions = [
|
|
"modified",
|
|
"mod",
|
|
"lastModified",
|
|
"lastMod",
|
|
"lastmodified",
|
|
"lastmod",
|
|
"updated",
|
|
];
|
|
for (var possibleKey in modifiedKeyOptions) {
|
|
var val = data.props[possibleKey];
|
|
if (val != null) {
|
|
if (val is int) {
|
|
modified = parseUnixTimeStamp(val);
|
|
settings.modifiedFormat = DateFormat.UnixTimeStamp;
|
|
} else {
|
|
modified = parseDateTime(val.toString());
|
|
settings.modifiedFormat = DateFormat.Iso8601;
|
|
}
|
|
settings.modifiedKey = possibleKey;
|
|
|
|
propsUsed.add(possibleKey);
|
|
break;
|
|
}
|
|
}
|
|
if (modified == null) {
|
|
settings.modifiedFormat = DateFormat.None;
|
|
}
|
|
|
|
var body = settings.emojify ? emojiParser.emojify(data.body) : data.body;
|
|
|
|
DateTime? created;
|
|
var createdKeyOptions = [
|
|
"created",
|
|
"date",
|
|
];
|
|
for (var possibleKey in createdKeyOptions) {
|
|
var val = data.props[possibleKey];
|
|
if (val != null) {
|
|
if (val is int) {
|
|
created = parseUnixTimeStamp(val);
|
|
settings.createdFormat = DateFormat.UnixTimeStamp;
|
|
} else {
|
|
created = parseDateTime(val.toString());
|
|
settings.createdFormat = DateFormat.Iso8601;
|
|
}
|
|
settings.createdKey = possibleKey;
|
|
|
|
propsUsed.add(possibleKey);
|
|
break;
|
|
}
|
|
}
|
|
if (created == null) {
|
|
settings.createdFormat = DateFormat.None;
|
|
}
|
|
|
|
//
|
|
// Title parsing
|
|
//
|
|
String? title;
|
|
if (data.props.containsKey(settings.titleKey)) {
|
|
title = data.props[settings.titleKey]?.toString() ?? "";
|
|
title = settings.emojify ? emojiParser.emojify(title) : title;
|
|
|
|
propsUsed.add(settings.titleKey);
|
|
settings.titleSettings = SettingsTitle.InYaml;
|
|
} else {
|
|
var startsWithH1 = false;
|
|
for (var line in LineSplitter.split(body)) {
|
|
if (line.trim().isEmpty) {
|
|
continue;
|
|
}
|
|
startsWithH1 = line.startsWith('#') && !line.startsWith('##');
|
|
break;
|
|
}
|
|
|
|
if (startsWithH1) {
|
|
var titleStartIndex = body.indexOf('#');
|
|
var titleEndIndex = body.indexOf('\n', titleStartIndex);
|
|
if (titleEndIndex == -1 || titleEndIndex == body.length) {
|
|
title = body.substring(titleStartIndex + 1).trim();
|
|
body = "";
|
|
} else {
|
|
title = body.substring(titleStartIndex + 1, titleEndIndex).trim();
|
|
body = body.substring(titleEndIndex + 1).trim();
|
|
}
|
|
}
|
|
}
|
|
|
|
NoteType? type;
|
|
var typeStr = data.props[settings.typeKey];
|
|
switch (typeStr) {
|
|
case "Checklist":
|
|
type = NoteType.Checklist;
|
|
break;
|
|
case "Journal":
|
|
type = NoteType.Journal;
|
|
break;
|
|
default:
|
|
type = NoteType.Unknown;
|
|
break;
|
|
}
|
|
if (typeStr != null) {
|
|
propsUsed.add(settings.typeKey);
|
|
}
|
|
|
|
Set<String>? _tags;
|
|
try {
|
|
var tagKeyOptions = [
|
|
"tags",
|
|
"categories",
|
|
"keywords",
|
|
];
|
|
for (var possibleKey in tagKeyOptions) {
|
|
var tags = data.props[possibleKey];
|
|
if (tags != null) {
|
|
if (tags is YamlList) {
|
|
_tags = tags.map((t) => t.toString()).toSet();
|
|
} else if (tags is List) {
|
|
_tags = tags.map((t) => t.toString()).toSet();
|
|
} else if (tags is String) {
|
|
settings.tagsInString = true;
|
|
var allTags = tags.split(' ');
|
|
settings.tagsHaveHash = allTags.every((t) => t.startsWith('#'));
|
|
if (settings.tagsHaveHash) {
|
|
allTags.removeWhere((e) => e.length <= 1);
|
|
allTags = allTags.map((e) => e.substring(1)).toList();
|
|
}
|
|
|
|
_tags = allTags.toSet();
|
|
} else {
|
|
Log.e("Note Tags Decoding Failed: $tags");
|
|
}
|
|
|
|
settings.tagsKey = possibleKey;
|
|
propsUsed.add(settings.tagsKey);
|
|
break;
|
|
}
|
|
}
|
|
} catch (e) {
|
|
Log.e("Note Decoding Failed: $e");
|
|
}
|
|
|
|
// Extra Props
|
|
var extraProps = <String, dynamic>{};
|
|
data.props.forEach((key, val) {
|
|
if (propsUsed.contains(key)) {
|
|
return;
|
|
}
|
|
|
|
extraProps[key] = val;
|
|
});
|
|
|
|
return Note.build(
|
|
parent: parent,
|
|
file: file,
|
|
modified: modified,
|
|
created: created,
|
|
body: body,
|
|
title: title ?? "",
|
|
noteType: type,
|
|
extraProps: extraProps,
|
|
tags: _tags ?? {},
|
|
doc: data,
|
|
serializerSettings: settings,
|
|
fileFormat: NoteFileFormat.Markdown,
|
|
);
|
|
}
|
|
}
|