import 'dart:io'; import 'package:flutter/material.dart'; import 'package:path/path.dart' as p; import 'package:path/path.dart'; import 'note.dart'; import 'note_fs_entity.dart'; class NotesFolder with ChangeNotifier implements Comparable { final NotesFolder parent; String folderPath; List _entities = []; Map _entityMap = {}; NotesFolder(this.parent, this.folderPath); @override void dispose() { _entities.forEach((e) { if (e.isFolder) { e.folder.removeListener(_entityChanged); } else { e.note.removeListener(_entityChanged); } }); super.dispose(); } void _entityChanged() { notifyListeners(); } bool get isEmpty { return _entities.isEmpty; } String get name { return basename(folderPath); } bool get hasSubFolders { return _entities.firstWhere((e) => e.isFolder, orElse: () => null) != null; } bool get hasNotes { return _entities.firstWhere((e) => e.isNote, orElse: () => null) != null; } bool get hasNotesRecursive { bool has = hasNotes; if (has) return true; for (var i = 0; i < _entities.length; i++) { var e = _entities[i]; if (e.isNote) continue; has = has || e.folder.hasNotes; if (has) { return true; } } return has; } int get numberOfNotes { int i = 0; _entities.forEach((e) { if (e.isNote) i++; }); return i; } List getNotes() { return _entities.where((e) => e.isNote).map((e) => e.note).toList(); } List getFolders() { var list = _entities.where((e) => e.isFolder).map((e) => e.folder).toList(); list.sort(); return list; } // FIXME: This asynchronously loads everything. Maybe it should just list them, and the individual _entities // should be loaded as required? Future loadRecursively() async { await load(); _entities.forEach((e) { if (e.isFolder) { e.folder.loadRecursively(); } else { // 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 e.note.load(); } }); } // FIXME: This should not reconstruct the Notes or NotesFolders once constructed. Future load() async { Set pathsFound = {}; var entitiesAdded = false; var entitiesRemoved = false; 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 = NotesFolder(this, fsEntity.path); if (subFolder.name.startsWith('.')) { continue; } subFolder.addListener(_entityChanged); var noteFSEntity = NoteFSEntity(folder: subFolder); _entities.add(noteFSEntity); _entityMap[fsEntity.path] = noteFSEntity; pathsFound.add(fsEntity.path); entitiesAdded = true; continue; } var note = Note(this, fsEntity.path); if (!note.filePath.toLowerCase().endsWith('.md')) { continue; } note.addListener(_entityChanged); var noteFSEntity = NoteFSEntity(note: note); _entities.add(noteFSEntity); _entityMap[fsEntity.path] = noteFSEntity; pathsFound.add(fsEntity.path); entitiesAdded = true; } Set pathsRemoved = _entityMap.keys.toSet().difference(pathsFound); pathsRemoved.forEach((path) { var e = _entityMap[path]; assert(e != null); if (e.isFolder) { e.folder.removeListener(_entityChanged); } else { e.note.removeListener(_entityChanged); } _entityMap.remove(path); }); _entities.removeWhere((e) { String path = e.isFolder ? e.folder.folderPath : e.note.filePath; return pathsRemoved.contains(path); }); entitiesRemoved = pathsRemoved.isNotEmpty; if (entitiesAdded || entitiesRemoved) { notifyListeners(); } } void add(Note note) { assert(note.parent == this); note.addListener(_entityChanged); var entity = NoteFSEntity(note: note); _entities.add(entity); _entityMap[note.filePath] = entity; notifyListeners(); } void insert(int index, Note note) { assert(note.parent == this); assert(index >= 0); note.addListener(_entityChanged); if (_entities.isEmpty) { var entity = NoteFSEntity(note: note); _entities.add(entity); _entityMap[note.filePath] = entity; notifyListeners(); return; } for (var i = 0; i < _entities.length; i++) { var e = _entities[i]; if (e is NotesFolder) continue; if (index == 0) { var entity = NoteFSEntity(note: note); _entities.insert(i, entity); _entityMap[note.filePath] = entity; notifyListeners(); return; } index--; } } void remove(Note note) { assert(note.parent == this); note.removeListener(_entityChanged); var i = _entities.indexWhere((e) { if (e.isFolder) return false; return e.note.filePath == note.filePath; }); assert(i != -1); _entities.removeAt(i); _entityMap.remove(note.filePath); notifyListeners(); } 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(NotesFolder folder) { assert(folder.parent == this); folder.addListener(_entityChanged); var entity = NoteFSEntity(folder: folder); _entities.add(entity); _entityMap[folder.folderPath] = entity; notifyListeners(); } void removeFolder(NotesFolder folder) { folder.removeListener(_entityChanged); var i = _entities.indexWhere((e) { if (e.isNote) return false; return e.folder.folderPath == folder.folderPath; }); assert(i != -1); _entities.removeAt(i); _entityMap.remove(folder.folderPath); notifyListeners(); } void rename(String newName) { var dir = Directory(folderPath); var parentDirName = dirname(folderPath); dir.renameSync(folderPath); folderPath = p.join(parentDirName, newName); notifyListeners(); } String pathSpec() { if (parent == null) { return ""; } return p.join(parent.pathSpec(), name); } @override int compareTo(NotesFolder other) { return folderPath.compareTo(other.folderPath); } }