/* * SPDX-FileCopyrightText: 2019-2021 Vishesh Handa * * 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 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 _tags = {}; Map _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 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? extraProps, Set? 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 get tags { return _tags; } Map 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"); }