From 7bd690a807b27a15dfd4d932571f46f644ca4e83 Mon Sep 17 00:00:00 2001 From: randogoth Date: Sat, 22 Feb 2025 16:57:29 +0200 Subject: [PATCH] optional file content / subdirs --- lib/note_list.dart | 126 +++++++++++++++++++++++++++++---------------- 1 file changed, 81 insertions(+), 45 deletions(-) diff --git a/lib/note_list.dart b/lib/note_list.dart index 7bdd260..c276c44 100644 --- a/lib/note_list.dart +++ b/lib/note_list.dart @@ -30,9 +30,12 @@ class _NoteListScreenState extends State { TextEditingController searchController = TextEditingController(); List notes = []; List filteredNotes = []; + Map> noteTags = {}; String? notesDirectoryPath; String? defaultDir; bool _useExternalEditor = false; + bool _includeFileContent = false; + bool _includeSubdirectories = true; @override void initState() { @@ -81,11 +84,22 @@ class _NoteListScreenState extends State { setState(() { defaultDir = dir.path; _useExternalEditor = prefs.getBool('use_external_editor') ?? false; + _includeSubdirectories = prefs.getBool('include_subdirectories') ?? true; // ✅ Load setting }); await _loadNotes(); } + Future _toggleIncludeSubdirectories(bool value) async { + setState(() { + _includeSubdirectories = value; + }); + final prefs = await SharedPreferences.getInstance(); + await prefs.setBool('include_subdirectories', value); + _loadNotes(); // Reload notes based on new setting + } + + void _showDirectoryChoiceDialog(String defaultPath) { showDialog( context: context, @@ -140,48 +154,35 @@ class _NoteListScreenState extends State { Future _loadNotes() async { final prefs = await SharedPreferences.getInstance(); notesDirectoryPath = prefs.getString('notes_directory') ?? defaultDir; - final notesDir = Directory(notesDirectoryPath!); + if (!notesDir.existsSync()) { notesDir.createSync(recursive: true); } List allNotes = []; - Map folderTimestamps = {}; // Store latest edit time per folder + Map folderTimestamps = {}; void fetchNotes(Directory dir, String relativePath) { - final entries = dir.listSync(recursive: false); + final entries = dir.listSync(recursive: _includeSubdirectories); // ✅ Only include subfolders if enabled 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); @@ -193,46 +194,53 @@ class _NoteListScreenState extends State { }); } - void _filterNotes() { - final query = searchController.text.trim(); - if (query.isEmpty) { - setState(() { - filteredNotes = notes; + /// Extracts unique tags sorted by +, @, # + Set _extractTags(String content) { + final tagPattern = RegExp(r'([#@+][a-zA-Z][a-zA-Z0-9_]*)'); + final matches = tagPattern.allMatches(content).map((m) => m.group(0)!).toSet(); + + // Sort the tags in order: +tags first, @contexts second, #hashtags last + final sortedTags = matches.toList() + ..sort((a, b) { + const order = {'+': 0, '@': 1, '#': 2}; + return (order[a[0]] ?? 3).compareTo(order[b[0]] ?? 3); }); - return; - } - final List words = query.split(RegExp(r'\s+')); // Split by spaces - final List terms = []; - final List tags = []; + return sortedTags.toSet(); // Convert back to a set after sorting + } - for (final word in words) { - if (word.startsWith('#') || word.startsWith('+') || word.startsWith('@')) { - tags.add(word.toLowerCase()); - } else { - terms.add(word.toLowerCase()); - } - } + void _filterNotes() { + final query = searchController.text.trim().toLowerCase(); setState(() { + if (query.isEmpty) { + filteredNotes = List.from(notes); + return; + } + + final terms = query.split(' '); // Split search into words + final searchTags = terms.where((t) => t.startsWith(RegExp(r'[#@+]'))).toSet(); + final searchText = terms.where((t) => !t.startsWith(RegExp(r'[#@+]'))).join(' '); + filteredNotes = notes.where((note) { - final content = note.readAsStringSync().toLowerCase(); final filename = note.uri.pathSegments.last.toLowerCase(); + final content = note.readAsStringSync().toLowerCase(); + final noteTags = _extractTags(content); - // Check if all terms are found in filename or content - final bool matchesTerms = terms.isEmpty || - terms.every((term) => filename.contains(term) || content.contains(term)); + // ✅ Match partial tags instead of requiring full matches + final matchesTags = searchTags.every((tag) => + noteTags.any((noteTag) => noteTag.startsWith(tag)) // ✅ Partial match + ); - // Check if all tags are found in content - final bool matchesTags = tags.isEmpty || - tags.every((tag) => content.contains(tag)); - - return matchesTerms && matchesTags; + if (!_includeFileContent) { + return filename.contains(searchText) && matchesTags; + } else { + return (filename.contains(searchText) || content.contains(searchText)) && matchesTags; + } }).toList(); }); } - List _formatTags(String content) { final tagPattern = RegExp(r'([#@+][a-zA-Z0-9_]+)'); final Set tagSet = {}; // Avoid duplicate tags @@ -542,7 +550,7 @@ class _NoteListScreenState extends State { value: 'Toggle Tags', child: ListTile( leading: - Icon(widget.showTags ? Icons.label_off : Icons.label), + Icon(widget.showTags ? Icons.short_text : Icons.tag), title: Text(widget.showTags ? 'Hide Tags' : 'Show Tags'), ), ), @@ -561,6 +569,34 @@ class _NoteListScreenState extends State { ), ), ), + PopupMenuItem( + value: 'Include Subdirectories', + child: ListTile( + leading: Icon(_includeSubdirectories ? Icons.file_copy_outlined : Icons.folder_copy_outlined), + title: Text(_includeSubdirectories ? 'Exclude Subdirectories' : 'Include Subdirectories'), + onTap: () { + _toggleIncludeSubdirectories(!_includeSubdirectories); + Navigator.pop(context); // Close menu after toggle + }, + ), + ), + PopupMenuItem( + value: 'Include File Content', + child: ListTile( + leading: Icon( + _includeFileContent ? Icons.highlight_off : Icons.file_open_outlined, + ), + title: Text( + _includeFileContent ? 'Exclude File Content' : 'Include File Content', + ), + onTap: () { + setState(() { + _includeFileContent = !_includeFileContent; + }); + Navigator.pop(context); + }, + ), + ), PopupMenuItem( value: 'External Editor', child: ListTile( @@ -579,7 +615,7 @@ class _NoteListScreenState extends State { PopupMenuItem( value: 'Select Folder', child: ListTile( - leading: Icon(Icons.folder), + leading: Icon(Icons.folder_shared_outlined), title: Text('Set Notes Directory'), ), ),