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

80
lib/texts.dart Normal file → Executable file
View file

@ -3,63 +3,82 @@ String helpContent = """
Type to filter notes by title. Type to filter notes by title.
Use #tags, +projects, or @contexts to find notes with specific tags. Use #tags, +projects, or @contexts to find notes with specific tags.
Start with a / to filter notes within /subdirectories. Start with a / to filter notes within /folders and /sub/folders.
Mix text, tags, and folders (/work project #important). Mix text, tags, and folders freely (/work project #important).
# Create Notes # Create Notes
Type the title of a new note in the search bar and hit Enter. Type the title of a new note in the search bar and hit Enter.
Use /folder/note title to create a note inside a subdirectory. Use /folder/note to create a note inside a subdirectory.
Subfolders will be automatically created if they dont exist. Subfolders are automatically created if they don't exist.
# Delete Notes # Open Notes
Swipe a note in the list to the left to move it to the trash. **Tap** a note to open it in View mode (syntax-highlighted, read-only).
The trash is a directory in the root folder. **Long-press** a note to open it scrolled to the end of the file.
Use :emptytrash or the context menu button to permanently clear deleted notes. Switch between View, Edit, and Preview modes from the editor menu.
# Edit Notes # Edit Notes
Enable/disable autosave in the menu. When disabled you can use the Save button in the menu. Switch to Edit mode from the editor menu. The cursor is placed at the end.
If there are unsaved changes when you are leaving the editor, you will be prompted to save. Enable/disable Autosave in the editor menu.
When Autosave is off, use the Save button or you will be prompted on exit.
Adjust the font size with the slider in the editor menu.
Share a note as plain text from the editor menu.
# Favorites
Swipe a note to the **right** to mark it as a favorite (gold star).
Swipe right again to remove it from favorites.
Use *Show Favorites* in the menu to filter the list to starred notes only.
# Delete Notes
Swipe a note to the **left** to move it to the trash.
The trash is a directory in the root notes folder.
Use :emptytrash or the context menu to permanently clear deleted notes.
# Tag Notes # Tag Notes
Use tags anywhere in the notes and they will be detected. Use tags anywhere in note content they are detected automatically.
#tags General categorization. #tags General categorization.
+projects Project-based grouping. +projects Project-based grouping.
@contexts Context-specific notes. @contexts Context-specific notes.
Tags must start with a letter and be preceded by a space or line break.
# Context Menu # Context Menu
Tap the orange blaster at the top right to open the Preferences. Tap the orange blaster at the top right to open the preferences menu.
Here you find toggles to show/hide the tags below the note titles and switch between light and dark themes. - *Show Favorites / Show All Notes* Filter the list to starred notes.
- *Hide/Show Tags* Show tag chips below note titles in the list.
You can also exclude subdirectories from your search. - *Switch to Light/Dark Mode* Toggle themes.
- *Exclude/Search Subdirectories* Control the depth of your search.
By default the search bar filters by note titles, folders, and tags, but you can also toggle including the file contents. Please be aware that this might slow down the app if you have many files. - *Search/Exclude File Content* Also search inside note content (may slow down the app with many files).
- *Use External/Internal Editor* Open notes in your preferred editor app.
If you prefer to use a different Markdown editor for your files you can toggle that and when you tap a file in the list you can choose what to open the file with. - *Set Notes Directory* Choose a folder anywhere on the device.
- *Empty Trash* Permanently delete all trashed notes.
By default the notes are saved in the App's own directory which is inaccessible to other apps. If you want to choose your own custom folder for this, you can set it. You will be asked for special permissions to do so. This will enable you to save the notes wherever you want, for example to sync them with a different app. You can also choose a folder that already has notes in it. - *Help* Show this guide.
- *About* App credits and version.
# File Organization # File Organization
Root notes are listed above subfolder notes. Root notes are listed above subfolder notes.
Subfolders are ordered by the latest modified file inside them. Subfolders are ordered by the latest modified file inside them.
Empty folders auto-delete after the last file is removed. Empty folders are automatically removed after the last file is deleted.
# Commands # Commands
Use commands by starting with a colon : Type a colon command in the search bar and press Enter:
:help Open this guide. :help Open this guide.
:tags Toggle tag visibility. :tags Toggle tag visibility.
:day - Switch to light theme. :day Switch to light theme.
:night - Switch to dark theme. :night Switch to dark theme.
:extern Toggle external editor usage. :extern Toggle external editor.
:intern Toggle internal editor usage. :intern Toggle internal editor.
:emptytrash Permanently delete trashed notes. :emptytrash Permanently delete trashed notes.
:about Open app information. :about Open app information.
@ -67,12 +86,11 @@ Use commands by starting with a colon :
"""; """;
String aboutContent = """ String aboutContent = """
**Muzzle Velocity** is a fast and minimalist note-taking app inspired by *Notational Velocity* and *Terminal Velocity*. v.1.1.0
**Muzzle Velocity** is a fast and minimalist but opinionated note-taking app inspired by the legendary *Notational Velocity* on MacOS and *Terminal Velocity* for command line terminals.
It is built for speed and efficiency, eliminating unnecessary UI clutter. Every action should be as fast and frictionless as possible. It is built for speed and efficiency, eliminating unnecessary UI clutter. Every action should be as fast and frictionless as possible.
Made with 🧡 by randogoth Made with 🧡 by randogoth
with the invaluable help of
OpenAI's 🤖 ChatGPT
"""; """;

View file

@ -1,7 +1,7 @@
name: muzzle_velocity name: muzzle_velocity
description: "A new Flutter project." description: "A new Flutter project."
publish_to: 'none' # Remove this line if you wish to publish to pub.dev publish_to: 'none' # Remove this line if you wish to publish to pub.dev
version: 1.0.0+1 version: 1.1.0+2
environment: environment:
sdk: ^3.6.0 sdk: ^3.6.0