diff --git a/lib/note_editor.dart b/lib/note_editor.dart index b3e9ca5..2ddd64d 100644 --- a/lib/note_editor.dart +++ b/lib/note_editor.dart @@ -58,8 +58,10 @@ class _NoteEditorScreenState extends State { 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); diff --git a/lib/note_list.dart b/lib/note_list.dart index d1c7dec..ca31da6 100644 --- a/lib/note_list.dart +++ b/lib/note_list.dart @@ -139,33 +139,82 @@ class _NoteListScreenState extends State { Future _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() - .where((file) => file.path.endsWith('.md')) - .toList(); + List allNotes = []; + Map 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 { 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 { } 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 { } 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 { ); } +// ✅ 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 _emptyTrash() async { final prefs = await SharedPreferences.getInstance(); final notesPath = prefs.getString('notes_directory') ?? @@ -461,50 +591,61 @@ class _NoteListScreenState extends State { 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 + } } - }, ), ); },