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 {
|
try {
|
||||||
final notesDirectory = widget.notesDirectoryPath ??
|
final notesDirectory = widget.notesDirectoryPath ??
|
||||||
widget.note.parent.path; // ✅ Use correct directory
|
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");
|
print("📝 Attempting to save note at: $newFilePath");
|
||||||
final newFile = File(newFilePath);
|
final newFile = File(newFilePath);
|
||||||
|
|
|
||||||
|
|
@ -139,33 +139,82 @@ class _NoteListScreenState extends State<NoteListScreen> {
|
||||||
|
|
||||||
Future<void> _loadNotes() async {
|
Future<void> _loadNotes() async {
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
notesDirectoryPath = prefs.getString('notes_directory') ??
|
notesDirectoryPath = prefs.getString('notes_directory') ?? defaultDir;
|
||||||
defaultDir; // Use custom or default directory
|
|
||||||
|
|
||||||
final notesDir = Directory(notesDirectoryPath!);
|
final notesDir = Directory(notesDirectoryPath!);
|
||||||
if (!notesDir.existsSync()) {
|
if (!notesDir.existsSync()) {
|
||||||
notesDir.createSync(recursive: true);
|
notesDir.createSync(recursive: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
final files = notesDir
|
List<File> allNotes = [];
|
||||||
.listSync()
|
Map<String, DateTime> folderTimestamps = {}; // Store latest edit time per folder
|
||||||
.whereType<File>()
|
|
||||||
.where((file) => file.path.endsWith('.md'))
|
void fetchNotes(Directory dir, String relativePath) {
|
||||||
.toList();
|
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(() {
|
setState(() {
|
||||||
notes = files;
|
notes = allNotes;
|
||||||
filteredNotes = files;
|
filteredNotes = allNotes;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void _filterNotes() {
|
void _filterNotes() {
|
||||||
final query = searchController.text.toLowerCase();
|
final query = searchController.text.trim().toLowerCase();
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
filteredNotes = notes.where((note) {
|
if (query.startsWith('/')) {
|
||||||
final content = note.readAsStringSync().toLowerCase();
|
// Extract subfolder path and search term
|
||||||
return note.uri.pathSegments.last.toLowerCase().contains(query) ||
|
final parts = query.split('/');
|
||||||
content.contains(query);
|
final subfolderPath = parts.sublist(1, parts.length - 1).join('/');
|
||||||
}).toList();
|
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 {
|
void _createNewNote(String title) async {
|
||||||
try {
|
try {
|
||||||
final safeTitle = _sanitizeFilename(title);
|
String notePath = title.trim();
|
||||||
final newNote = File(path.join(notesDirectoryPath!, '$safeTitle.md'));
|
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()) {
|
if (!newNote.existsSync()) {
|
||||||
newNote.writeAsStringSync('# $title\n\n');
|
newNote.writeAsStringSync('# $safeTitle\n\n');
|
||||||
setState(() {
|
setState(() {
|
||||||
notes.add(newNote);
|
notes.add(newNote);
|
||||||
filteredNotes.add(newNote);
|
filteredNotes.add(newNote);
|
||||||
});
|
});
|
||||||
|
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
|
|
@ -218,8 +299,27 @@ class _NoteListScreenState extends State<NoteListScreen> {
|
||||||
}
|
}
|
||||||
|
|
||||||
bool _isNewNote(String title) {
|
bool _isNewNote(String title) {
|
||||||
final safeTitle = _sanitizeFilename(title);
|
String notePath = title.trim();
|
||||||
return !notes.any((note) => note.uri.pathSegments.last == '$safeTitle.md');
|
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) {
|
String _sanitizeFilename(String title) {
|
||||||
|
|
@ -229,13 +329,29 @@ class _NoteListScreenState extends State<NoteListScreen> {
|
||||||
}
|
}
|
||||||
|
|
||||||
void _deleteNote(File note) {
|
void _deleteNote(File note) {
|
||||||
final trashDir = Directory('${note.parent.path}/trash');
|
final rootTrashDir = Directory(path.join(notesDirectoryPath!, 'trash'));
|
||||||
if (!trashDir.existsSync()) {
|
|
||||||
trashDir.createSync(recursive: true);
|
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);
|
note.renameSync(trashedNote.path);
|
||||||
|
|
||||||
|
// Check if the original directory is now empty and delete it if needed
|
||||||
|
_deleteEmptyParentFolders(note.parent);
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
notes.remove(note);
|
notes.remove(note);
|
||||||
filteredNotes.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 {
|
Future<void> _emptyTrash() async {
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
final notesPath = prefs.getString('notes_directory') ??
|
final notesPath = prefs.getString('notes_directory') ??
|
||||||
|
|
@ -461,50 +591,61 @@ class _NoteListScreenState extends State<NoteListScreen> {
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
title:
|
title:
|
||||||
Text(noteFile.uri.pathSegments.last.replaceAll('.md', '')),
|
Text(noteFile.uri.pathSegments.last.replaceAll('.md', '')),
|
||||||
subtitle: widget.showTags && tagSpans.isNotEmpty
|
subtitle: Row(
|
||||||
? RichText(
|
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(
|
text: TextSpan(
|
||||||
style: DefaultTextStyle.of(context).style,
|
style: DefaultTextStyle.of(context).style,
|
||||||
children: tagSpans))
|
children: tagSpans)
|
||||||
: null,
|
)
|
||||||
onTap: () async {
|
: Text("")
|
||||||
if (_useExternalEditor) {
|
],
|
||||||
// Open with system's default editor
|
),
|
||||||
try {
|
onTap: () async {
|
||||||
if (Platform.isLinux) {
|
if (_useExternalEditor) {
|
||||||
final result = await Process.run('which', ['xdg-open']);
|
// Open with system's default editor
|
||||||
if (result.stdout.toString().trim().isNotEmpty) {
|
try {
|
||||||
await Process.run('xdg-open', [noteFile.path]);
|
if (Platform.isLinux) {
|
||||||
} else {
|
final result = await Process.run('which', ['xdg-open']);
|
||||||
print("❌ `xdg-open` is not available.");
|
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) {
|
} catch (e) {
|
||||||
await _requestStoragePermission();
|
print("❌ Failed to open external editor: $e");
|
||||||
final result = await OpenFilex.open(
|
|
||||||
noteFile.path,
|
|
||||||
type: "text/markdown",
|
|
||||||
);
|
|
||||||
print("✅ OpenFilex result: ${result.type}, message: ${result.message}");
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} else {
|
||||||
print("❌ Failed to open external editor: $e");
|
// Open internally in NoteEditorScreen
|
||||||
}
|
await Navigator.push(
|
||||||
} else {
|
context,
|
||||||
// Open internally
|
MaterialPageRoute(
|
||||||
await Navigator.push(
|
builder: (context) => NoteEditorScreen(
|
||||||
context,
|
note: noteFile, // ✅ Pass the full file path
|
||||||
MaterialPageRoute(
|
isDarkMode: widget.isDarkMode,
|
||||||
builder: (context) => NoteEditorScreen(
|
notesDirectoryPath: notesDirectoryPath,
|
||||||
note: File(path.join(notesDirectoryPath!, noteFile.uri.pathSegments.last)),
|
),
|
||||||
isDarkMode: widget.isDarkMode,
|
|
||||||
notesDirectoryPath: notesDirectoryPath,
|
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
);
|
_loadNotes(); // Reload notes after returning
|
||||||
_loadNotes(); // Reload notes after returning
|
setState(() {}); // Force UI refresh
|
||||||
setState(() {}); // Force UI refresh
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue