subfolder support
This commit is contained in:
parent
a7f2e1ca89
commit
f77a66d5d8
2 changed files with 208 additions and 65 deletions
|
|
@ -58,8 +58,10 @@ class _NoteEditorScreenState extends State<NoteEditorScreen> {
|
|||
try {
|
||||
final notesDirectory = widget.notesDirectoryPath ??
|
||||
widget.note.parent.path; // ✅ Use correct directory
|
||||
final newFilePath =
|
||||
path.join(notesDirectory, widget.note.uri.pathSegments.last);
|
||||
|
||||
// Get the full path of the original file, preserving subfolder structure
|
||||
final relativeNotePath = path.relative(widget.note.path, from: notesDirectory);
|
||||
final newFilePath = path.join(notesDirectory, relativeNotePath); // ✅ Preserve subfolder
|
||||
|
||||
print("📝 Attempting to save note at: $newFilePath");
|
||||
final newFile = File(newFilePath);
|
||||
|
|
|
|||
|
|
@ -139,33 +139,82 @@ class _NoteListScreenState extends State<NoteListScreen> {
|
|||
|
||||
Future<void> _loadNotes() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
notesDirectoryPath = prefs.getString('notes_directory') ??
|
||||
defaultDir; // Use custom or default directory
|
||||
notesDirectoryPath = prefs.getString('notes_directory') ?? defaultDir;
|
||||
|
||||
final notesDir = Directory(notesDirectoryPath!);
|
||||
if (!notesDir.existsSync()) {
|
||||
notesDir.createSync(recursive: true);
|
||||
}
|
||||
|
||||
final files = notesDir
|
||||
.listSync()
|
||||
.whereType<File>()
|
||||
.where((file) => file.path.endsWith('.md'))
|
||||
.toList();
|
||||
List<File> allNotes = [];
|
||||
Map<String, DateTime> folderTimestamps = {}; // Store latest edit time per folder
|
||||
|
||||
void fetchNotes(Directory dir, String relativePath) {
|
||||
final entries = dir.listSync(recursive: false);
|
||||
|
||||
for (var entry in entries) {
|
||||
if (entry is File && entry.path.endsWith('.md')) {
|
||||
allNotes.add(entry);
|
||||
|
||||
// Store latest edit time per folder
|
||||
final folder = path.dirname(entry.path);
|
||||
if (!folderTimestamps.containsKey(folder) ||
|
||||
entry.lastModifiedSync().isAfter(folderTimestamps[folder]!)) {
|
||||
folderTimestamps[folder] = entry.lastModifiedSync();
|
||||
}
|
||||
} else if (entry is Directory && path.basename(entry.path) != 'trash') {
|
||||
fetchNotes(entry, path.join(relativePath, path.basename(entry.path)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fetchNotes(notesDir, '');
|
||||
|
||||
// Sorting logic:
|
||||
allNotes.sort((a, b) {
|
||||
String folderA = path.dirname(a.path);
|
||||
String folderB = path.dirname(b.path);
|
||||
|
||||
bool isRootA = folderA == notesDirectoryPath; // Root folder check
|
||||
bool isRootB = folderB == notesDirectoryPath;
|
||||
|
||||
if (isRootA && !isRootB) return -1; // Root files first
|
||||
if (!isRootA && isRootB) return 1; // Subfolder files later
|
||||
|
||||
// If both are in the root OR both are in subfolders, sort by latest edit time
|
||||
DateTime timeA = folderTimestamps[folderA] ?? DateTime(0);
|
||||
DateTime timeB = folderTimestamps[folderB] ?? DateTime(0);
|
||||
return timeB.compareTo(timeA);
|
||||
});
|
||||
|
||||
setState(() {
|
||||
notes = files;
|
||||
filteredNotes = files;
|
||||
notes = allNotes;
|
||||
filteredNotes = allNotes;
|
||||
});
|
||||
}
|
||||
|
||||
void _filterNotes() {
|
||||
final query = searchController.text.toLowerCase();
|
||||
final query = searchController.text.trim().toLowerCase();
|
||||
|
||||
setState(() {
|
||||
filteredNotes = notes.where((note) {
|
||||
final content = note.readAsStringSync().toLowerCase();
|
||||
return note.uri.pathSegments.last.toLowerCase().contains(query) ||
|
||||
content.contains(query);
|
||||
}).toList();
|
||||
if (query.startsWith('/')) {
|
||||
// Extract subfolder path and search term
|
||||
final parts = query.split('/');
|
||||
final subfolderPath = parts.sublist(1, parts.length - 1).join('/');
|
||||
final searchTerm = parts.last;
|
||||
|
||||
filteredNotes = notes.where((note) {
|
||||
final relativePath = path.relative(note.path, from: notesDirectoryPath!);
|
||||
return relativePath.startsWith(subfolderPath) &&
|
||||
(relativePath.contains(searchTerm) || note.readAsStringSync().toLowerCase().contains(searchTerm));
|
||||
}).toList();
|
||||
} else {
|
||||
// Default search across all notes
|
||||
filteredNotes = notes.where((note) {
|
||||
final content = note.readAsStringSync().toLowerCase();
|
||||
return note.uri.pathSegments.last.toLowerCase().contains(query) || content.contains(query);
|
||||
}).toList();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -188,15 +237,47 @@ class _NoteListScreenState extends State<NoteListScreen> {
|
|||
|
||||
void _createNewNote(String title) async {
|
||||
try {
|
||||
final safeTitle = _sanitizeFilename(title);
|
||||
final newNote = File(path.join(notesDirectoryPath!, '$safeTitle.md'));
|
||||
String notePath = title.trim();
|
||||
String? subfolder;
|
||||
String safeTitle;
|
||||
|
||||
if (notePath.startsWith('/')) {
|
||||
final parts = notePath.split('/');
|
||||
parts.removeWhere((element) => element.isEmpty);
|
||||
if (parts.length > 1) {
|
||||
subfolder = parts.sublist(0, parts.length - 1).join('/');
|
||||
}
|
||||
safeTitle = _sanitizeFilename(parts.last);
|
||||
} else {
|
||||
safeTitle = _sanitizeFilename(title);
|
||||
}
|
||||
|
||||
// Construct the final file path
|
||||
String fullPath = subfolder != null
|
||||
? path.join(notesDirectoryPath!, subfolder, '$safeTitle.md')
|
||||
: path.join(notesDirectoryPath!, '$safeTitle.md');
|
||||
|
||||
final newNote = File(fullPath);
|
||||
|
||||
// Prevent duplicate note creation
|
||||
if (notes.any((note) => note.path == newNote.path)) {
|
||||
print("❌ Note already exists at: $fullPath");
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure subfolder exists
|
||||
final parentDir = newNote.parent;
|
||||
if (!parentDir.existsSync()) {
|
||||
parentDir.createSync(recursive: true);
|
||||
}
|
||||
|
||||
if (!newNote.existsSync()) {
|
||||
newNote.writeAsStringSync('# $title\n\n');
|
||||
newNote.writeAsStringSync('# $safeTitle\n\n');
|
||||
setState(() {
|
||||
notes.add(newNote);
|
||||
filteredNotes.add(newNote);
|
||||
});
|
||||
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
|
|
@ -218,8 +299,27 @@ class _NoteListScreenState extends State<NoteListScreen> {
|
|||
}
|
||||
|
||||
bool _isNewNote(String title) {
|
||||
final safeTitle = _sanitizeFilename(title);
|
||||
return !notes.any((note) => note.uri.pathSegments.last == '$safeTitle.md');
|
||||
String notePath = title.trim();
|
||||
String? subfolder;
|
||||
String safeTitle;
|
||||
|
||||
if (notePath.startsWith('/')) {
|
||||
final parts = notePath.split('/');
|
||||
parts.removeWhere((element) => element.isEmpty); // Remove empty elements
|
||||
if (parts.length > 1) {
|
||||
subfolder = parts.sublist(0, parts.length - 1).join('/');
|
||||
}
|
||||
safeTitle = _sanitizeFilename(parts.last);
|
||||
} else {
|
||||
safeTitle = _sanitizeFilename(title);
|
||||
}
|
||||
|
||||
// Construct the expected file path
|
||||
String fullPath = subfolder != null
|
||||
? path.join(notesDirectoryPath!, subfolder, '$safeTitle.md')
|
||||
: path.join(notesDirectoryPath!, '$safeTitle.md');
|
||||
|
||||
return !notes.any((note) => note.path == fullPath);
|
||||
}
|
||||
|
||||
String _sanitizeFilename(String title) {
|
||||
|
|
@ -229,13 +329,29 @@ class _NoteListScreenState extends State<NoteListScreen> {
|
|||
}
|
||||
|
||||
void _deleteNote(File note) {
|
||||
final trashDir = Directory('${note.parent.path}/trash');
|
||||
if (!trashDir.existsSync()) {
|
||||
trashDir.createSync(recursive: true);
|
||||
final rootTrashDir = Directory(path.join(notesDirectoryPath!, 'trash'));
|
||||
|
||||
if (!rootTrashDir.existsSync()) {
|
||||
rootTrashDir.createSync(recursive: true);
|
||||
}
|
||||
final trashedNote = File('${trashDir.path}/${note.uri.pathSegments.last}');
|
||||
|
||||
// Define the base trashed file path
|
||||
String trashedPath = path.join(rootTrashDir.path, note.uri.pathSegments.last);
|
||||
File trashedNote = File(trashedPath);
|
||||
|
||||
// If the file already exists in trash, append a timestamp to avoid conflicts
|
||||
if (trashedNote.existsSync()) {
|
||||
final timestamp = DateTime.now().millisecondsSinceEpoch;
|
||||
final newFileName = '${path.basenameWithoutExtension(note.path)}_$timestamp.md';
|
||||
trashedNote = File(path.join(rootTrashDir.path, newFileName));
|
||||
}
|
||||
|
||||
// Move the file to trash
|
||||
note.renameSync(trashedNote.path);
|
||||
|
||||
// Check if the original directory is now empty and delete it if needed
|
||||
_deleteEmptyParentFolders(note.parent);
|
||||
|
||||
setState(() {
|
||||
notes.remove(note);
|
||||
filteredNotes.remove(note);
|
||||
|
|
@ -258,6 +374,20 @@ class _NoteListScreenState extends State<NoteListScreen> {
|
|||
);
|
||||
}
|
||||
|
||||
// ✅ Recursively deletes empty parent folders up to the notes directory
|
||||
void _deleteEmptyParentFolders(Directory folder) {
|
||||
if (folder.path == notesDirectoryPath) return; // Don't delete the root folder
|
||||
|
||||
try {
|
||||
if (folder.existsSync() && folder.listSync().isEmpty) {
|
||||
folder.deleteSync();
|
||||
_deleteEmptyParentFolders(folder.parent); // Recursively check parent
|
||||
}
|
||||
} catch (e) {
|
||||
print("❌ Error deleting empty folder: $e");
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _emptyTrash() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final notesPath = prefs.getString('notes_directory') ??
|
||||
|
|
@ -461,50 +591,61 @@ class _NoteListScreenState extends State<NoteListScreen> {
|
|||
child: ListTile(
|
||||
title:
|
||||
Text(noteFile.uri.pathSegments.last.replaceAll('.md', '')),
|
||||
subtitle: widget.showTags && tagSpans.isNotEmpty
|
||||
? RichText(
|
||||
subtitle: Row(
|
||||
children: [
|
||||
path.relative(path.dirname(noteFile.path), from: notesDirectoryPath!) != "."
|
||||
? Text(
|
||||
path.relative(path.dirname(noteFile.path), from: notesDirectoryPath!),
|
||||
style: TextStyle(color: Colors.blueGrey),
|
||||
)
|
||||
: Text(""),
|
||||
widget.showTags && tagSpans.isNotEmpty
|
||||
? RichText(
|
||||
text: TextSpan(
|
||||
style: DefaultTextStyle.of(context).style,
|
||||
children: tagSpans))
|
||||
: null,
|
||||
onTap: () async {
|
||||
if (_useExternalEditor) {
|
||||
// Open with system's default editor
|
||||
try {
|
||||
if (Platform.isLinux) {
|
||||
final result = await Process.run('which', ['xdg-open']);
|
||||
if (result.stdout.toString().trim().isNotEmpty) {
|
||||
await Process.run('xdg-open', [noteFile.path]);
|
||||
} else {
|
||||
print("❌ `xdg-open` is not available.");
|
||||
style: DefaultTextStyle.of(context).style,
|
||||
children: tagSpans)
|
||||
)
|
||||
: Text("")
|
||||
],
|
||||
),
|
||||
onTap: () async {
|
||||
if (_useExternalEditor) {
|
||||
// Open with system's default editor
|
||||
try {
|
||||
if (Platform.isLinux) {
|
||||
final result = await Process.run('which', ['xdg-open']);
|
||||
if (result.stdout.toString().trim().isNotEmpty) {
|
||||
await Process.run('xdg-open', [noteFile.path]); // ✅ Use full path
|
||||
} else {
|
||||
print("❌ `xdg-open` is not available.");
|
||||
}
|
||||
} else if (Platform.isAndroid) {
|
||||
await _requestStoragePermission();
|
||||
final result = await OpenFilex.open(
|
||||
noteFile.path, // ✅ Use full path
|
||||
type: "text/markdown",
|
||||
);
|
||||
print("✅ OpenFilex result: ${result.type}, message: ${result.message}");
|
||||
}
|
||||
} else if (Platform.isAndroid) {
|
||||
await _requestStoragePermission();
|
||||
final result = await OpenFilex.open(
|
||||
noteFile.path,
|
||||
type: "text/markdown",
|
||||
);
|
||||
print("✅ OpenFilex result: ${result.type}, message: ${result.message}");
|
||||
} catch (e) {
|
||||
print("❌ Failed to open external editor: $e");
|
||||
}
|
||||
} catch (e) {
|
||||
print("❌ Failed to open external editor: $e");
|
||||
}
|
||||
} else {
|
||||
// Open internally
|
||||
await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => NoteEditorScreen(
|
||||
note: File(path.join(notesDirectoryPath!, noteFile.uri.pathSegments.last)),
|
||||
isDarkMode: widget.isDarkMode,
|
||||
notesDirectoryPath: notesDirectoryPath,
|
||||
} else {
|
||||
// Open internally in NoteEditorScreen
|
||||
await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => NoteEditorScreen(
|
||||
note: noteFile, // ✅ Pass the full file path
|
||||
isDarkMode: widget.isDarkMode,
|
||||
notesDirectoryPath: notesDirectoryPath,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
_loadNotes(); // Reload notes after returning
|
||||
setState(() {}); // Force UI refresh
|
||||
);
|
||||
_loadNotes(); // Reload notes after returning
|
||||
setState(() {}); // Force UI refresh
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue