v1.1.0: favorites, note modes, long-press, search improvements

Bump version to 1.1.0+2.

New features since 1.0.x:
- Three editor modes: View (read-only, syntax-highlighted), Edit, Preview
- Favorites: swipe right to star a note; Show Favorites filter in menu
- Long-press a note to open scrolled to end; Edit mode always places
  cursor at end of file
- Subdirectory note creation (subdir/notename) and search (/subdir)
- Tag search (#tag @context +project) with case-insensitive matching
- Tag highlighting no longer falsely matches phone numbers / emails
- Font size slider and Share button in editor menu
- Search cache: note metadata indexed at load time, no per-keystroke
  file I/O
- App ID changed to com.randogoth.muzzlevelocity

Update Help and About texts to document all current features.
Update QuickStart note for new installs.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
randogoth 2026-06-19 19:09:14 +03:00
parent 14d6b3c44d
commit 8645c656f2
3 changed files with 140 additions and 61 deletions

View file

@ -48,6 +48,8 @@ class _NoteListScreenState extends State<NoteListScreen> {
bool _includeFileContent = false;
bool _includeSubdirectories = true;
Timer? _debounce;
Set<String> _favorites = {};
bool _showOnlyFavorites = false;
@override
void initState() {
@ -90,6 +92,23 @@ class _NoteListScreenState extends State<NoteListScreen> {
}
Future<void> _saveFavorites() async {
final prefs = await SharedPreferences.getInstance();
await prefs.setStringList('favorites', _favorites.toList());
}
void _toggleFavorite(File note) {
setState(() {
if (_favorites.contains(note.path)) {
_favorites.remove(note.path);
} else {
_favorites.add(note.path);
}
});
_saveFavorites();
_filterNotes();
}
Future<void> _toggleExternalEditor(bool value) async {
setState(() {
_useExternalEditor = value;
@ -124,6 +143,7 @@ class _NoteListScreenState extends State<NoteListScreen> {
defaultDir = dir.path;
_useExternalEditor = prefs.getBool('use_external_editor') ?? false;
_includeSubdirectories = prefs.getBool('include_subdirectories') ?? true;
_favorites = (prefs.getStringList('favorites') ?? []).toSet();
});
notesDirectoryPath = prefs.getString('notes_directory') ?? defaultDir;
@ -149,11 +169,13 @@ class _NoteListScreenState extends State<NoteListScreen> {
Welcome to *Muzzle Velocity*.
This is your fast and minimal note-taking app.
1. Create Notes quickly: just type a title in the search bar and press enter.
2. 🔍 Type keywords to filter notes instantly.
3. 💻 Type commands like :help to get more information.
4. 🏷 Use hashtags like #quickstart, context tags like @help, or project tags like +muzzle in your notes for categorization.
5. 🔫 Tap the blaster at the top right for more settings or to clear the search field.
1. **Create** a note: type a title in the search bar and press Enter.
2. 🔍 **Search** by title, #tag, +project, @context, or /subfolder.
3. **Favorite** a note: swipe it to the right.
4. 🗑 **Delete** a note: swipe it to the left.
5. 📖 **View / Edit / 👁 Preview**: switch modes from the editor menu.
6. 💻 **Commands**: type :help in the search bar for more information.
7. 🔫 Tap the blaster at the top right for settings.
*Muzzle Velocity* is built for speed and simplicity! 🚀
@ -326,30 +348,35 @@ Happy note taking! ✨
setState(() {
if (query.isEmpty) {
filteredNotes = List.from(notes);
return;
} else {
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(' ');
// Strip leading '/' so "/subdir" and "subdir" match the same notes
final normalizedSearch = searchText.replaceFirst(RegExp(r'^/'), '');
filteredNotes = notes.where((note) {
final relativePath = _noteRelativePaths[note] ?? '';
final tags = noteTags[note] ?? {};
final content = _includeFileContent ? (_noteContentLower[note] ?? '') : '';
final matchesTags = searchTags.every((tag) =>
tags.any((noteTag) => noteTag.toLowerCase().startsWith(tag)));
if (!_includeFileContent) {
return relativePath.contains(normalizedSearch) && matchesTags;
} else {
return (relativePath.contains(normalizedSearch) || content.contains(normalizedSearch)) && matchesTags;
}
}).toList();
}
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(' ');
// Strip leading '/' so "/subdir" and "subdir" match the same notes
final normalizedSearch = searchText.replaceFirst(RegExp(r'^/'), '');
filteredNotes = notes.where((note) {
final relativePath = _noteRelativePaths[note] ?? '';
final tags = noteTags[note] ?? {};
final content = _includeFileContent ? (_noteContentLower[note] ?? '') : '';
final matchesTags = searchTags.every((tag) =>
tags.any((noteTag) => noteTag.toLowerCase().startsWith(tag)));
if (!_includeFileContent) {
return relativePath.contains(normalizedSearch) && matchesTags;
} else {
return (relativePath.contains(normalizedSearch) || content.contains(normalizedSearch)) && matchesTags;
}
}).toList();
if (_showOnlyFavorites) {
filteredNotes = filteredNotes
.where((note) => _favorites.contains(note.path))
.toList();
}
});
}
@ -717,6 +744,10 @@ Happy note taking! ✨
),
onSelected: (String value) {
switch (value) {
case 'Toggle Favorites':
setState(() { _showOnlyFavorites = !_showOnlyFavorites; });
_filterNotes();
break;
case 'Toggle Tags':
widget.toggleTags();
break;
@ -744,6 +775,17 @@ Happy note taking! ✨
}
},
itemBuilder: (BuildContext context) => [
PopupMenuItem<String>(
value: 'Toggle Favorites',
child: ListTile(
leading: Icon(
_showOnlyFavorites ? Icons.star : Icons.star_outline,
color: _showOnlyFavorites ? Colors.amber : null,
),
title: Text(_showOnlyFavorites ? 'Show All Notes' : 'Show Favorites'),
),
),
PopupMenuDivider(),
PopupMenuItem<String>(
value: 'Toggle Tags',
child: ListTile(
@ -869,16 +911,35 @@ Happy note taking! ✨
final noteFile = filteredNotes[index];
return Dismissible(
key: Key(noteFile.path),
direction: DismissDirection.endToStart,
direction: DismissDirection.horizontal,
background: Container(
color: Colors.amber,
alignment: Alignment.centerLeft,
padding: EdgeInsets.symmetric(horizontal: 20),
child: Icon(
_favorites.contains(noteFile.path) ? Icons.star_outline : Icons.star,
color: Colors.white,
),
),
secondaryBackground: Container(
color: Colors.redAccent,
alignment: Alignment.centerRight,
padding: EdgeInsets.symmetric(horizontal: 20),
child: Icon(Icons.delete, color: Colors.white),
),
confirmDismiss: (direction) async {
if (direction == DismissDirection.startToEnd) {
_toggleFavorite(noteFile);
return false;
}
return true;
},
onDismissed: (direction) => _deleteNote(noteFile),
child: ListTile(
title: Text(noteFile.uri.pathSegments.last.replaceAll('.md', '')),
trailing: _favorites.contains(noteFile.path)
? Icon(Icons.star, color: Colors.amber, size: 18)
: null,
subtitle: LayoutBuilder(
builder: (context, constraints) {
final bool hasEnoughSpace = constraints.maxWidth > 200; // Adjust threshold