mirror of
https://github.com/GitJournal/GitJournal.git
synced 2025-09-14 15:33:17 +08:00

With this 'txt' files are now read and written. However, they don't have any metadata so they don't show up correctly in the sort order and currently the Markdown Editor is still available for them. Related to #55
508 lines
12 KiB
Dart
508 lines
12 KiB
Dart
import 'dart:io';
|
|
|
|
import 'package:easy_localization/easy_localization.dart';
|
|
import 'package:gitjournal/features.dart';
|
|
import 'package:gitjournal/utils/logger.dart';
|
|
|
|
import 'package:path/path.dart' as p;
|
|
import 'package:path/path.dart';
|
|
import 'package:synchronized/synchronized.dart';
|
|
|
|
import 'note.dart';
|
|
import 'notes_folder.dart';
|
|
import 'notes_folder_notifier.dart';
|
|
|
|
class NotesFolderFS with NotesFolderNotifier implements NotesFolder {
|
|
final NotesFolderFS _parent;
|
|
String _folderPath;
|
|
var _lock = Lock();
|
|
|
|
List<Note> _notes = [];
|
|
List<NotesFolderFS> _folders = [];
|
|
|
|
Map<String, dynamic> _entityMap = {};
|
|
NotesFolderConfig _config;
|
|
|
|
NotesFolderFS(this._parent, this._folderPath);
|
|
|
|
@override
|
|
void dispose() {
|
|
_folders.forEach((f) => f.removeListener(_entityChanged));
|
|
_notes.forEach((f) => f.removeListener(_entityChanged));
|
|
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
NotesFolder get parent => _parent;
|
|
|
|
NotesFolderFS get parentFS => _parent;
|
|
|
|
void _entityChanged() {
|
|
notifyListeners();
|
|
}
|
|
|
|
void _noteModified(Note note) {
|
|
notifyNoteModified(-1, note);
|
|
}
|
|
|
|
void _noteRenamed(Note note, String oldPath) {
|
|
assert(_entityMap.containsKey(oldPath));
|
|
_entityMap.remove(oldPath);
|
|
_entityMap[note.filePath] = note;
|
|
|
|
notifyNoteRenamed(-1, note, oldPath);
|
|
}
|
|
|
|
void _subFolderRenamed(NotesFolderFS folder, String oldPath) {
|
|
assert(_entityMap.containsKey(oldPath));
|
|
_entityMap.remove(oldPath);
|
|
_entityMap[folder.folderPath] = folder;
|
|
}
|
|
|
|
void reset(String folderPath) {
|
|
_folderPath = folderPath;
|
|
|
|
var notesCopy = List<Note>.from(_notes);
|
|
notesCopy.forEach(remove);
|
|
|
|
var foldersCopy = List<NotesFolderFS>.from(_folders);
|
|
foldersCopy.forEach(removeFolder);
|
|
|
|
assert(_notes.isEmpty);
|
|
assert(_folders.isEmpty);
|
|
|
|
notifyListeners();
|
|
}
|
|
|
|
String get folderPath => _folderPath;
|
|
|
|
@override
|
|
bool get isEmpty {
|
|
return _notes.isEmpty && _folders.isEmpty;
|
|
}
|
|
|
|
@override
|
|
String get name => basename(folderPath);
|
|
|
|
bool get hasSubFolders {
|
|
return _folders.isNotEmpty;
|
|
}
|
|
|
|
@override
|
|
bool get hasNotes {
|
|
return _notes.isNotEmpty;
|
|
}
|
|
|
|
bool get hasNotesRecursive {
|
|
if (_notes.isNotEmpty) {
|
|
return true;
|
|
}
|
|
|
|
for (var folder in _folders) {
|
|
if (folder.hasNotesRecursive) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
int get numberOfNotes {
|
|
return _notes.length;
|
|
}
|
|
|
|
@override
|
|
List<Note> get notes {
|
|
return _notes;
|
|
}
|
|
|
|
@override
|
|
List<NotesFolder> get subFolders => subFoldersFS;
|
|
|
|
List<NotesFolderFS> get subFoldersFS {
|
|
// FIXME: This is really not ideal
|
|
_folders.sort((NotesFolderFS a, NotesFolderFS b) =>
|
|
a.folderPath.compareTo(b.folderPath));
|
|
return _folders;
|
|
}
|
|
|
|
// FIXME: This asynchronously loads everything. Maybe it should just list them, and the individual _entities
|
|
// should be loaded as required?
|
|
Future<void> loadRecursively() async {
|
|
const maxParallel = 10;
|
|
var futures = <Future>[];
|
|
|
|
await load();
|
|
|
|
for (var note in _notes) {
|
|
// FIXME: Collected all the Errors, and report them back, along with "WHY", and the contents of the Note
|
|
// Each of these needs to be reported to crashlytics, as Note loading should never fail
|
|
var f = note.load();
|
|
futures.add(f);
|
|
|
|
if (futures.length >= maxParallel) {
|
|
await Future.wait(futures);
|
|
futures = <Future>[];
|
|
}
|
|
}
|
|
|
|
await Future.wait(futures);
|
|
futures = <Future>[];
|
|
|
|
for (var folder in _folders) {
|
|
var f = folder.loadRecursively();
|
|
futures.add(f);
|
|
}
|
|
|
|
return Future.wait(futures);
|
|
}
|
|
|
|
Future<void> load() async {
|
|
return _lock.synchronized(() async {
|
|
return _load();
|
|
});
|
|
}
|
|
|
|
// FIXME: This should not reconstruct the Notes or NotesFolders once constructed.
|
|
Future<void> _load() async {
|
|
Set<String> pathsFound = {};
|
|
|
|
// Load the Folder config if exists
|
|
if (Features.perFolderConfig) {
|
|
_config = await NotesFolderConfig.fromFS(this);
|
|
}
|
|
|
|
final dir = Directory(folderPath);
|
|
var lister = dir.list(recursive: false, followLinks: false);
|
|
await for (var fsEntity in lister) {
|
|
if (fsEntity is Link) {
|
|
continue;
|
|
}
|
|
|
|
// If already seen before
|
|
var existingNoteFSEntity = _entityMap[fsEntity.path];
|
|
if (existingNoteFSEntity != null) {
|
|
pathsFound.add(fsEntity.path);
|
|
continue;
|
|
}
|
|
|
|
if (fsEntity is Directory) {
|
|
var subFolder = NotesFolderFS(this, fsEntity.path);
|
|
if (subFolder.name.startsWith('.')) {
|
|
Log.v("Ignoring Folder", props: {
|
|
"path": fsEntity.path,
|
|
"reason": "Hidden folder",
|
|
});
|
|
continue;
|
|
}
|
|
Log.v("Found Folder", props: {"path": fsEntity.path});
|
|
_addFolderListeners(subFolder);
|
|
|
|
_folders.add(subFolder);
|
|
_entityMap[fsEntity.path] = subFolder;
|
|
|
|
pathsFound.add(fsEntity.path);
|
|
notifyFolderAdded(_folders.length - 1, subFolder);
|
|
continue;
|
|
}
|
|
|
|
var note = Note(this, fsEntity.path);
|
|
if (note.fileName.startsWith('.')) {
|
|
Log.v("Ignoring file", props: {
|
|
"path": fsEntity.path,
|
|
"reason": "Starts with a .",
|
|
});
|
|
continue;
|
|
}
|
|
var noteFilePath = note.filePath.toLowerCase();
|
|
var isMarkdownFile = noteFilePath.endsWith('.md');
|
|
var isTxtFile = noteFilePath.endsWith('.txt');
|
|
if (!isMarkdownFile && !isTxtFile) {
|
|
Log.v("Ignoring file", props: {
|
|
"path": fsEntity.path,
|
|
"reason": "Doesn't end with .md or .txt",
|
|
});
|
|
continue;
|
|
}
|
|
Log.v("Found file", props: {"path": fsEntity.path});
|
|
_addNoteListeners(note);
|
|
|
|
_notes.add(note);
|
|
_entityMap[fsEntity.path] = note;
|
|
|
|
pathsFound.add(fsEntity.path);
|
|
notifyNoteAdded(_notes.length - 1, note);
|
|
}
|
|
|
|
Set<String> pathsRemoved = _entityMap.keys.toSet().difference(pathsFound);
|
|
pathsRemoved.forEach((path) {
|
|
var e = _entityMap[path];
|
|
assert(e != null);
|
|
|
|
assert(e is NotesFolder || e is Note);
|
|
_entityMap.remove(path);
|
|
|
|
if (e is Note) {
|
|
Log.v("File $path was no longer found");
|
|
_removeNoteListeners(e);
|
|
|
|
var i = _notes.indexWhere((n) => n.filePath == path);
|
|
assert(i != -1);
|
|
var note = _notes[i];
|
|
_notes.removeAt(i);
|
|
notifyNoteRemoved(i, note);
|
|
} else {
|
|
Log.v("Folder $path was no longer found");
|
|
_removeFolderListeners(e);
|
|
|
|
var i = _folders.indexWhere((f) => f.folderPath == path);
|
|
assert(i != -1);
|
|
var folder = _folders[i];
|
|
_folders.removeAt(i);
|
|
notifyFolderRemoved(i, folder);
|
|
}
|
|
});
|
|
}
|
|
|
|
void add(Note note) {
|
|
assert(note.parent == this);
|
|
_addNoteListeners(note);
|
|
|
|
_notes.add(note);
|
|
_entityMap[note.filePath] = note;
|
|
|
|
notifyNoteAdded(_notes.length - 1, note);
|
|
}
|
|
|
|
void remove(Note note) {
|
|
assert(note.parent == this);
|
|
_removeNoteListeners(note);
|
|
|
|
assert(_notes.indexWhere((n) => n.filePath == note.filePath) != -1);
|
|
assert(_entityMap.containsKey(note.filePath));
|
|
|
|
var index = _notes.indexWhere((n) => n.filePath == note.filePath);
|
|
assert(index != -1);
|
|
_notes.removeAt(index);
|
|
_entityMap.remove(note.filePath);
|
|
|
|
notifyNoteRemoved(index, note);
|
|
}
|
|
|
|
void _addNoteListeners(Note note) {
|
|
note.addModifiedListener(_noteModified);
|
|
note.addRenameListener(_noteRenamed);
|
|
}
|
|
|
|
void _removeNoteListeners(Note note) {
|
|
note.removeModifiedListener(_noteModified);
|
|
note.removeRenameListener(_noteRenamed);
|
|
}
|
|
|
|
void create() {
|
|
// Git doesn't track Directories, only files, so we create an empty .gitignore file
|
|
// in the directory instead.
|
|
var gitIgnoreFilePath = p.join(folderPath, ".gitignore");
|
|
var file = File(gitIgnoreFilePath);
|
|
if (!file.existsSync()) {
|
|
file.createSync(recursive: true);
|
|
}
|
|
notifyListeners();
|
|
}
|
|
|
|
void addFolder(NotesFolderFS folder) {
|
|
assert(folder.parent == this);
|
|
_addFolderListeners(folder);
|
|
|
|
_folders.add(folder);
|
|
_entityMap[folder.folderPath] = folder;
|
|
|
|
notifyFolderAdded(_folders.length - 1, folder);
|
|
}
|
|
|
|
void removeFolder(NotesFolderFS folder) {
|
|
var notesCopy = List<Note>.from(folder._notes);
|
|
notesCopy.forEach(folder.remove);
|
|
|
|
var foldersCopy = List<NotesFolderFS>.from(folder._folders);
|
|
foldersCopy.forEach(folder.removeFolder);
|
|
|
|
_removeFolderListeners(folder);
|
|
|
|
assert(_folders.indexWhere((f) => f.folderPath == folder.folderPath) != -1);
|
|
assert(_entityMap.containsKey(folder.folderPath));
|
|
|
|
var index = _folders.indexWhere((f) => f.folderPath == folder.folderPath);
|
|
assert(index != -1);
|
|
_folders.removeAt(index);
|
|
_entityMap.remove(folder.folderPath);
|
|
|
|
notifyFolderRemoved(index, folder);
|
|
}
|
|
|
|
void rename(String newName) {
|
|
var oldPath = _folderPath;
|
|
var dir = Directory(_folderPath);
|
|
_folderPath = p.join(dirname(_folderPath), newName);
|
|
dir.renameSync(_folderPath);
|
|
|
|
notifyThisFolderRenamed(this, oldPath);
|
|
}
|
|
|
|
void _addFolderListeners(NotesFolderFS folder) {
|
|
folder.addListener(_entityChanged);
|
|
folder.addThisFolderRenamedListener(_subFolderRenamed);
|
|
}
|
|
|
|
void _removeFolderListeners(NotesFolderFS folder) {
|
|
folder.removeListener(_entityChanged);
|
|
folder.removeThisFolderRenamedListener(_subFolderRenamed);
|
|
}
|
|
|
|
@override
|
|
String pathSpec() {
|
|
if (parent == null) {
|
|
return "";
|
|
}
|
|
return p.join(parent.pathSpec(), name);
|
|
}
|
|
|
|
@override
|
|
String get publicName {
|
|
var spec = pathSpec();
|
|
if (spec.isEmpty) {
|
|
return tr("rootFolder");
|
|
}
|
|
return spec;
|
|
}
|
|
|
|
Iterable<Note> getAllNotes() sync* {
|
|
for (var note in _notes) {
|
|
yield note;
|
|
}
|
|
|
|
for (var folder in _folders) {
|
|
var notes = folder.getAllNotes();
|
|
for (var note in notes) {
|
|
yield note;
|
|
}
|
|
}
|
|
}
|
|
|
|
@override
|
|
NotesFolder get fsFolder {
|
|
return this;
|
|
}
|
|
|
|
NotesFolderFS getFolderWithSpec(String spec) {
|
|
if (pathSpec() == spec) {
|
|
return this;
|
|
}
|
|
for (var f in _folders) {
|
|
var res = f.getFolderWithSpec(spec);
|
|
if (res != null) {
|
|
return res;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
NotesFolderFS get rootFolder {
|
|
var folder = this;
|
|
while (folder.parent != null) {
|
|
folder = folder.parent;
|
|
}
|
|
return folder;
|
|
}
|
|
|
|
Note getNoteWithSpec(String spec) {
|
|
var parts = spec.split(p.separator);
|
|
var folder = this;
|
|
while (parts.length != 1) {
|
|
var folderName = parts[0];
|
|
|
|
bool foundFolder = false;
|
|
for (var f in _folders) {
|
|
if (f.name == folderName) {
|
|
folder = f;
|
|
foundFolder = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!foundFolder) {
|
|
return null;
|
|
}
|
|
parts.removeAt(0);
|
|
}
|
|
|
|
var fileName = parts[0];
|
|
for (var note in folder.notes) {
|
|
if (note.fileName == fileName) {
|
|
return note;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
@override
|
|
NotesFolderConfig get config {
|
|
if (Features.perFolderConfig && _config != null) {
|
|
return _config;
|
|
}
|
|
return NotesFolderConfig.fromSettings(this);
|
|
}
|
|
|
|
@override
|
|
set config(NotesFolderConfig config) {
|
|
if (Features.perFolderConfig) {
|
|
_config = config;
|
|
} else {
|
|
config.saveToSettings();
|
|
}
|
|
}
|
|
|
|
Set<String> getNoteTagsRecursively() {
|
|
return _fetchTags(this, {});
|
|
}
|
|
|
|
Future<List<Note>> matchNotes(NoteMatcherAsync pred) async {
|
|
var matchedNotes = <Note>[];
|
|
await _matchNotes(matchedNotes, pred);
|
|
return matchedNotes;
|
|
}
|
|
|
|
Future<List<Note>> _matchNotes(
|
|
List<Note> matchedNotes,
|
|
NoteMatcherAsync pred,
|
|
) async {
|
|
for (var note in _notes) {
|
|
var matches = await pred(note);
|
|
if (matches) {
|
|
matchedNotes.add(note);
|
|
}
|
|
}
|
|
|
|
for (var folder in _folders) {
|
|
await folder._matchNotes(matchedNotes, pred);
|
|
}
|
|
return matchedNotes;
|
|
}
|
|
}
|
|
|
|
typedef NoteMatcherAsync = Future<bool> Function(Note n);
|
|
|
|
Set<String> _fetchTags(NotesFolder folder, Set<String> tags) {
|
|
for (var note in folder.notes) {
|
|
tags.addAll(note.tags);
|
|
}
|
|
|
|
for (var folder in folder.subFolders) {
|
|
tags = _fetchTags(folder, tags);
|
|
}
|
|
|
|
return tags;
|
|
}
|