mirror of
https://github.com/GitJournal/GitJournal.git
synced 2025-08-06 15:21:21 +08:00
375 lines
8.7 KiB
Dart
375 lines
8.7 KiB
Dart
/*
|
|
* SPDX-FileCopyrightText: 2019-2021 Vishesh Handa <me@vhanda.in>
|
|
*
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
import 'package:path/path.dart' as p;
|
|
import 'package:universal_io/io.dart';
|
|
import 'package:uuid/uuid.dart';
|
|
|
|
import 'package:gitjournal/core/notes_folder_fs.dart';
|
|
import 'package:gitjournal/error_reporting.dart';
|
|
import 'package:gitjournal/logger/logger.dart';
|
|
import 'package:gitjournal/settings/settings.dart';
|
|
import 'package:gitjournal/utils/datetime.dart';
|
|
import 'md_yaml_doc.dart';
|
|
import 'note_serializer.dart';
|
|
|
|
typedef void NoteSelectedFunction(Note note);
|
|
typedef bool NoteBoolPropertyFunction(Note note);
|
|
|
|
/// Move this to NotesFolderFS
|
|
enum NoteLoadState {
|
|
None,
|
|
Loading,
|
|
Loaded,
|
|
NotExists,
|
|
Error,
|
|
}
|
|
|
|
enum NoteType { Unknown, Checklist, Journal, Org }
|
|
|
|
class NoteFileFormatInfo {
|
|
static List<String> allowedExtensions = ['.md', '.org', '.txt'];
|
|
|
|
static String defaultExtension(NoteFileFormat format) {
|
|
switch (format) {
|
|
case NoteFileFormat.Markdown:
|
|
return ".md";
|
|
case NoteFileFormat.OrgMode:
|
|
return '.org';
|
|
case NoteFileFormat.Txt:
|
|
return ".txt";
|
|
default:
|
|
return ".md";
|
|
}
|
|
}
|
|
|
|
static bool isAllowedFileName(String filePath) {
|
|
var noteFilePath = filePath.toLowerCase();
|
|
for (var ext in allowedExtensions) {
|
|
if (noteFilePath.endsWith(ext)) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// FIXME: Treat Markdown and Markdown + YAML differently
|
|
enum NoteFileFormat {
|
|
Markdown,
|
|
OrgMode,
|
|
Txt,
|
|
}
|
|
|
|
class Note {
|
|
NotesFolderFS parent;
|
|
String? _filePath;
|
|
|
|
String _title = "";
|
|
DateTime? _created;
|
|
DateTime? _modified;
|
|
String _body = "";
|
|
NoteType _type = NoteType.Unknown;
|
|
Set<String> _tags = {};
|
|
Map<String, dynamic> _extraProps = {};
|
|
|
|
NoteFileFormat? _fileFormat;
|
|
|
|
MdYamlDoc _data = MdYamlDoc();
|
|
late NoteSerializer noteSerializer;
|
|
|
|
DateTime fileLastModified;
|
|
|
|
var _loadState = NoteLoadState.None;
|
|
|
|
Note(this.parent, this._filePath, this.fileLastModified) {
|
|
var settings = NoteSerializationSettings.fromConfig(parent.config);
|
|
noteSerializer = NoteSerializer.fromConfig(settings);
|
|
}
|
|
|
|
Note.newNote(
|
|
this.parent, {
|
|
Map<String, dynamic> extraProps = const {},
|
|
String fileName = "",
|
|
}) : fileLastModified = DateTime.fromMillisecondsSinceEpoch(0) {
|
|
_created = DateTime.now();
|
|
_loadState = NoteLoadState.Loaded;
|
|
_fileFormat = NoteFileFormat.Markdown;
|
|
var settings = NoteSerializationSettings.fromConfig(parent.config);
|
|
noteSerializer = NoteSerializer.fromConfig(settings);
|
|
|
|
if (extraProps.isNotEmpty) {
|
|
extraProps.forEach((key, value) {
|
|
_data.props[key] = value;
|
|
});
|
|
noteSerializer.decode(_data, this);
|
|
}
|
|
|
|
if (fileName.isNotEmpty) {
|
|
// FIXME: We should ensure a note with this fileName does not already
|
|
// exist
|
|
if (!NoteFileFormatInfo.isAllowedFileName(fileName)) {
|
|
fileName +=
|
|
NoteFileFormatInfo.defaultExtension(NoteFileFormat.Markdown);
|
|
}
|
|
_filePath = p.join(parent.folderPath, fileName);
|
|
Log.i("Constructing new note with path $_filePath");
|
|
}
|
|
}
|
|
|
|
String get filePath {
|
|
if (_filePath == null) {
|
|
var fp = "";
|
|
try {
|
|
fp = p.join(parent.folderPath, _buildFileName());
|
|
} catch (e, stackTrace) {
|
|
Log.e("_buildFileName: $e");
|
|
logExceptionWarning(e, stackTrace);
|
|
fp = p.join(parent.folderPath, const Uuid().v4());
|
|
}
|
|
switch (_fileFormat) {
|
|
case NoteFileFormat.OrgMode:
|
|
if (!fp.toLowerCase().endsWith('.org')) {
|
|
fp += '.org';
|
|
}
|
|
break;
|
|
|
|
case NoteFileFormat.Txt:
|
|
if (!fp.toLowerCase().endsWith('.txt')) {
|
|
fp += '.txt';
|
|
}
|
|
break;
|
|
|
|
case NoteFileFormat.Markdown:
|
|
default:
|
|
if (!fp.toLowerCase().endsWith('.md')) {
|
|
fp += '.md';
|
|
}
|
|
break;
|
|
}
|
|
|
|
_filePath = fp;
|
|
}
|
|
|
|
return _filePath as String;
|
|
}
|
|
|
|
void apply({
|
|
String? filePath,
|
|
DateTime? created,
|
|
DateTime? modified,
|
|
String? body,
|
|
String? title,
|
|
NoteType? type,
|
|
Map<String, dynamic>? extraProps,
|
|
Set<String>? tags,
|
|
NoteFileFormat? fileFormat,
|
|
NoteLoadState? loadState,
|
|
}) {
|
|
var changed = false;
|
|
if (filePath != null) {
|
|
_filePath = filePath;
|
|
changed = true;
|
|
}
|
|
if (canHaveMetadata) {
|
|
if (created != null && created != _created) {
|
|
_created = created;
|
|
changed = true;
|
|
}
|
|
if (modified != null && modified != _modified) {
|
|
_modified = modified;
|
|
changed = true;
|
|
}
|
|
|
|
if (type != null && type != _type) {
|
|
_type = type;
|
|
changed = true;
|
|
}
|
|
|
|
if (extraProps != null) {
|
|
_extraProps = extraProps;
|
|
changed = true;
|
|
}
|
|
|
|
if (tags != null) {
|
|
_tags = tags;
|
|
changed = true;
|
|
}
|
|
}
|
|
|
|
if (body != null && body != _body) {
|
|
_body = body;
|
|
changed = true;
|
|
}
|
|
|
|
if (title != null && title != _title) {
|
|
_title = title;
|
|
changed = true;
|
|
}
|
|
|
|
if (fileFormat != null && _fileFormat != fileFormat) {
|
|
_fileFormat = fileFormat;
|
|
changed = true;
|
|
}
|
|
|
|
if (loadState != null && _loadState != loadState) {
|
|
_loadState = loadState;
|
|
changed = true;
|
|
}
|
|
|
|
if (changed) {
|
|
_notifyModified();
|
|
}
|
|
}
|
|
|
|
String get fileName {
|
|
return p.basename(filePath);
|
|
}
|
|
|
|
DateTime? get created {
|
|
return _created;
|
|
}
|
|
|
|
DateTime? get modified {
|
|
return _modified;
|
|
}
|
|
|
|
void updateModified() {
|
|
_modified = DateTime.now();
|
|
_notifyModified();
|
|
}
|
|
|
|
String get body {
|
|
return _body;
|
|
}
|
|
|
|
String get title {
|
|
return _title;
|
|
}
|
|
|
|
NoteType get type {
|
|
return _type;
|
|
}
|
|
|
|
Set<String> get tags {
|
|
return _tags;
|
|
}
|
|
|
|
Map<String, dynamic> get extraProps {
|
|
return _extraProps;
|
|
}
|
|
|
|
bool get canHaveMetadata {
|
|
if (_fileFormat == NoteFileFormat.Txt ||
|
|
_fileFormat == NoteFileFormat.OrgMode) {
|
|
return false;
|
|
}
|
|
return parent.config.yamlHeaderEnabled;
|
|
}
|
|
|
|
MdYamlDoc get data {
|
|
noteSerializer.encode(this, _data);
|
|
return _data;
|
|
}
|
|
|
|
set data(MdYamlDoc data) {
|
|
_data = data;
|
|
noteSerializer.decode(_data, this);
|
|
|
|
_notifyModified();
|
|
}
|
|
|
|
NoteLoadState get loadState {
|
|
return _loadState;
|
|
}
|
|
|
|
@override
|
|
int get hashCode => _filePath.hashCode;
|
|
|
|
@override
|
|
bool operator ==(Object other) =>
|
|
identical(this, other) ||
|
|
other is Note &&
|
|
runtimeType == other.runtimeType &&
|
|
_filePath == other._filePath &&
|
|
_data == other._data;
|
|
|
|
@override
|
|
String toString() {
|
|
return 'Note{filePath: $_filePath, created: $created, modified: $modified, data: $_data, loadState: $_loadState}';
|
|
}
|
|
|
|
void _notifyModified() {
|
|
parent.noteModified(this);
|
|
}
|
|
|
|
String pathSpec() {
|
|
return p.join(parent.pathSpec(), fileName);
|
|
}
|
|
|
|
String _buildFileName() {
|
|
var date = created ?? modified ?? fileLastModified;
|
|
var isJournal = type == NoteType.Journal;
|
|
switch (!isJournal
|
|
? parent.config.fileNameFormat
|
|
: parent.config.journalFileNameFormat) {
|
|
case NoteFileNameFormat.SimpleDate:
|
|
return toSimpleDateTime(date);
|
|
case NoteFileNameFormat.DateOnly:
|
|
var dateStr = toDateString(date);
|
|
return ensureFileNameUnique(parent.folderPath, dateStr, ".md");
|
|
case NoteFileNameFormat.FromTitle:
|
|
if (title.isNotEmpty) {
|
|
return buildTitleFileName(parent.folderPath, title);
|
|
} else {
|
|
return toSimpleDateTime(date);
|
|
}
|
|
case NoteFileNameFormat.Iso8601:
|
|
return toIso8601(date);
|
|
case NoteFileNameFormat.Iso8601WithTimeZone:
|
|
return toIso8601WithTimezone(date);
|
|
case NoteFileNameFormat.Iso8601WithTimeZoneWithoutColon:
|
|
return toIso8601WithTimezone(date).replaceAll(":", "_");
|
|
case NoteFileNameFormat.UuidV4:
|
|
return const Uuid().v4();
|
|
case NoteFileNameFormat.Zettelkasten:
|
|
return toZettleDateTime(date);
|
|
}
|
|
|
|
return date.toString();
|
|
}
|
|
|
|
NoteFileFormat? get fileFormat {
|
|
return _fileFormat;
|
|
}
|
|
}
|
|
|
|
String ensureFileNameUnique(String parentDir, String name, String ext) {
|
|
var fileName = name + ext;
|
|
var fullPath = p.join(parentDir, fileName);
|
|
var file = File(fullPath);
|
|
if (!file.existsSync()) {
|
|
return fileName;
|
|
}
|
|
|
|
for (var i = 1;; i++) {
|
|
var fileName = name + "_$i$ext";
|
|
var fullPath = p.join(parentDir, fileName);
|
|
var file = File(fullPath);
|
|
if (!file.existsSync()) {
|
|
return fileName;
|
|
}
|
|
}
|
|
}
|
|
|
|
String buildTitleFileName(String parentDir, String title) {
|
|
// Sanitize the title - these characters are not allowed in Windows
|
|
title = title.replaceAll(RegExp(r'[/<\>":|?*]'), '_');
|
|
|
|
return ensureFileNameUnique(parentDir, title, ".md");
|
|
}
|