optional file content / subdirs
This commit is contained in:
parent
004d2d9e82
commit
7bd690a807
1 changed files with 81 additions and 45 deletions
|
|
@ -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'),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue