optional file content / subdirs

This commit is contained in:
randogoth 2025-02-22 16:57:29 +02:00
parent 004d2d9e82
commit 7bd690a807

View file

@ -30,9 +30,12 @@ class _NoteListScreenState extends State<NoteListScreen> {
TextEditingController searchController = TextEditingController(); TextEditingController searchController = TextEditingController();
List<File> notes = []; List<File> notes = [];
List<File> filteredNotes = []; List<File> filteredNotes = [];
Map<File, Set<String>> noteTags = {};
String? notesDirectoryPath; String? notesDirectoryPath;
String? defaultDir; String? defaultDir;
bool _useExternalEditor = false; bool _useExternalEditor = false;
bool _includeFileContent = false;
bool _includeSubdirectories = true;
@override @override
void initState() { void initState() {
@ -81,11 +84,22 @@ class _NoteListScreenState extends State<NoteListScreen> {
setState(() { setState(() {
defaultDir = dir.path; defaultDir = dir.path;
_useExternalEditor = prefs.getBool('use_external_editor') ?? false; _useExternalEditor = prefs.getBool('use_external_editor') ?? false;
_includeSubdirectories = prefs.getBool('include_subdirectories') ?? true; // Load setting
}); });
await _loadNotes(); await _loadNotes();
} }
Future<void> _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) { void _showDirectoryChoiceDialog(String defaultPath) {
showDialog( showDialog(
context: context, context: context,
@ -140,48 +154,35 @@ 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') ?? defaultDir; notesDirectoryPath = prefs.getString('notes_directory') ?? defaultDir;
final notesDir = Directory(notesDirectoryPath!); final notesDir = Directory(notesDirectoryPath!);
if (!notesDir.existsSync()) { if (!notesDir.existsSync()) {
notesDir.createSync(recursive: true); notesDir.createSync(recursive: true);
} }
List<File> allNotes = []; List<File> allNotes = [];
Map<String, DateTime> folderTimestamps = {}; // Store latest edit time per folder Map<String, DateTime> folderTimestamps = {};
void fetchNotes(Directory dir, String relativePath) { 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) { for (var entry in entries) {
if (entry is File && entry.path.endsWith('.md')) { if (entry is File && entry.path.endsWith('.md')) {
allNotes.add(entry); allNotes.add(entry);
// Store latest edit time per folder
final folder = path.dirname(entry.path); final folder = path.dirname(entry.path);
if (!folderTimestamps.containsKey(folder) || if (!folderTimestamps.containsKey(folder) ||
entry.lastModifiedSync().isAfter(folderTimestamps[folder]!)) { entry.lastModifiedSync().isAfter(folderTimestamps[folder]!)) {
folderTimestamps[folder] = entry.lastModifiedSync(); 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, ''); fetchNotes(notesDir, '');
// Sorting logic:
allNotes.sort((a, b) { allNotes.sort((a, b) {
String folderA = path.dirname(a.path); String folderA = path.dirname(a.path);
String folderB = path.dirname(b.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 timeA = folderTimestamps[folderA] ?? DateTime(0);
DateTime timeB = folderTimestamps[folderB] ?? DateTime(0); DateTime timeB = folderTimestamps[folderB] ?? DateTime(0);
return timeB.compareTo(timeA); return timeB.compareTo(timeA);
@ -193,46 +194,53 @@ class _NoteListScreenState extends State<NoteListScreen> {
}); });
} }
void _filterNotes() { /// Extracts unique tags sorted by +, @, #
final query = searchController.text.trim(); Set<String> _extractTags(String content) {
if (query.isEmpty) { final tagPattern = RegExp(r'([#@+][a-zA-Z][a-zA-Z0-9_]*)');
setState(() { final matches = tagPattern.allMatches(content).map((m) => m.group(0)!).toSet();
filteredNotes = notes;
// 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<String> words = query.split(RegExp(r'\s+')); // Split by spaces return sortedTags.toSet(); // Convert back to a set after sorting
final List<String> terms = []; }
final List<String> tags = [];
for (final word in words) { void _filterNotes() {
if (word.startsWith('#') || word.startsWith('+') || word.startsWith('@')) { final query = searchController.text.trim().toLowerCase();
tags.add(word.toLowerCase());
} else {
terms.add(word.toLowerCase());
}
}
setState(() { 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) { filteredNotes = notes.where((note) {
final content = note.readAsStringSync().toLowerCase();
final filename = note.uri.pathSegments.last.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 // Match partial tags instead of requiring full matches
final bool matchesTerms = terms.isEmpty || final matchesTags = searchTags.every((tag) =>
terms.every((term) => filename.contains(term) || content.contains(term)); noteTags.any((noteTag) => noteTag.startsWith(tag)) // Partial match
);
// Check if all tags are found in content if (!_includeFileContent) {
final bool matchesTags = tags.isEmpty || return filename.contains(searchText) && matchesTags;
tags.every((tag) => content.contains(tag)); } else {
return (filename.contains(searchText) || content.contains(searchText)) && matchesTags;
return matchesTerms && matchesTags; }
}).toList(); }).toList();
}); });
} }
List<InlineSpan> _formatTags(String content) { List<InlineSpan> _formatTags(String content) {
final tagPattern = RegExp(r'([#@+][a-zA-Z0-9_]+)'); final tagPattern = RegExp(r'([#@+][a-zA-Z0-9_]+)');
final Set<String> tagSet = {}; // Avoid duplicate tags final Set<String> tagSet = {}; // Avoid duplicate tags
@ -542,7 +550,7 @@ class _NoteListScreenState extends State<NoteListScreen> {
value: 'Toggle Tags', value: 'Toggle Tags',
child: ListTile( child: ListTile(
leading: 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'), title: Text(widget.showTags ? 'Hide Tags' : 'Show Tags'),
), ),
), ),
@ -561,6 +569,34 @@ class _NoteListScreenState extends State<NoteListScreen> {
), ),
), ),
), ),
PopupMenuItem<String>(
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<String>(
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<String>( PopupMenuItem<String>(
value: 'External Editor', value: 'External Editor',
child: ListTile( child: ListTile(
@ -579,7 +615,7 @@ class _NoteListScreenState extends State<NoteListScreen> {
PopupMenuItem<String>( PopupMenuItem<String>(
value: 'Select Folder', value: 'Select Folder',
child: ListTile( child: ListTile(
leading: Icon(Icons.folder), leading: Icon(Icons.folder_shared_outlined),
title: Text('Set Notes Directory'), title: Text('Set Notes Directory'),
), ),
), ),