Compare commits

..

10 commits

Author SHA1 Message Date
randogoth
45dc458dc5 Add migration notice 2026-09-10 09:57:01 +02:00
randogoth
1a9513b704 set blaster SVG fill to orange (#FF9800)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-19 19:41:40 +03:00
randogoth
1ba414082e fix release link 2026-06-19 19:39:55 +03:00
randogoth
d6f8f14752 fix screenshot links to use full Codeberg URLs
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-19 19:35:58 +03:00
randogoth
01bf1396f8 add README with screenshots, features, and build instructions
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-19 19:34:37 +03:00
randogoth
3d7320b614 bump version to 1.1.1, update pubspec description
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-19 19:23:06 +03:00
randogoth
46dcd23419 persist font size, add :fav/:sub/:content commands
- note_editor: persist font size to SharedPreferences on change and
  restore it on load
- note_list: add :fav (toggle favorites filter), :sub (toggle
  subdirectory search), :content (toggle file content search) commands
- texts: document new commands in help content

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-19 19:13:56 +03:00
randogoth
8645c656f2 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>
2026-06-19 19:09:14 +03:00
randogoth
14d6b3c44d always place cursor at end of file when switching to edit mode
Removes the unreliable viewport-centre cursor heuristic. Switching to
edit mode now always positions the cursor at text.length and scrolls to
the bottom, whether arriving from a regular tap or a long-press. The
long-press scroll-to-end on initial load (view mode) is unchanged.

Cleans up _cursorAtEnd flag, _editorKey / GlobalKey, _getOffsetAtViewportCenter,
and the public WrappedCodeFieldState / findRenderEditable that were only
needed for the removed feature.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-19 19:01:21 +03:00
randogoth
db13702e54 add long-press gesture to open note scrolled to end
Long-pressing a note in the list opens it in view mode with the scroll
position at the bottom of the file. Switching to edit mode the first
time positions the cursor at the end of the text and scrolls there too,
so the user can immediately append content.

Implementation:
- NoteEditorScreen gains a scrollToEnd parameter (default false)
- A ScrollController is attached to the view/edit SingleChildScrollView
- After content loads with scrollToEnd=true, two post-frame callbacks
  fire (first renders the content, second has a stable maxScrollExtent)
  then jumpTo(maxScrollExtent)
- _cursorAtEnd flag is consumed on the first Mode Edit switch: sets
  selection to text.length, requests focus, and scrolls to bottom
- NoteListScreen ListTile gets onLongPress that navigates with
  scrollToEnd: true and reloads on return

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-19 17:50:56 +03:00
14 changed files with 269 additions and 83 deletions

0
.gitignore vendored Normal file → Executable file
View file

77
README.md Normal file → Executable file
View file

@ -1,16 +1,73 @@
# muzzlevelocity > **Migrated to [code.randogoth.com/randogoth/muzzle-velocity](https://code.randogoth.com/randogoth/muzzle-velocity)**
A new Flutter project. # Muzzle Velocity
## Getting Started <img src="assets/blaster.svg" width="96" alt="Muzzle Velocity logo">
This project is a starting point for a Flutter application. A fast and minimalist but opinionated note-taking app inspired by the legendary *Notational Velocity* on MacOS and *Terminal Velocity* for command line terminals.
A few resources to get you started if this is your first Flutter project: Built for speed and efficiency, eliminating unnecessary UI clutter. Every action should be as fast and frictionless as possible.
- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) ## Screenshots
- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)
For help getting started with Flutter development, view the <table>
[online documentation](https://docs.flutter.dev/), which offers tutorials, <tr>
samples, guidance on mobile development, and a full API reference. <td align="center"><a href="https://codeberg.org/randogoth/muzzle-velocity/src/branch/main/screenshots/01.jpg" target="_blank"><img src="screenshots/01.jpg" width="180"></a><br><sub>Search with tags</sub></td>
<td align="center"><a href="https://codeberg.org/randogoth/muzzle-velocity/src/branch/main/screenshots/02.jpg" target="_blank"><img src="screenshots/02.jpg" width="180"></a><br><sub>View mode</sub></td>
<td align="center"><a href="https://codeberg.org/randogoth/muzzle-velocity/src/branch/main/screenshots/03.jpg" target="_blank"><img src="screenshots/03.jpg" width="180"></a><br><sub>Editor menu</sub></td>
<td align="center"><a href="https://codeberg.org/randogoth/muzzle-velocity/src/branch/main/screenshots/04.jpg" target="_blank"><img src="screenshots/04.jpg" width="180"></a><br><sub>Preview mode</sub></td>
<td align="center"><a href="https://codeberg.org/randogoth/muzzle-velocity/src/branch/main/screenshots/05.jpg" target="_blank"><img src="screenshots/05.jpg" width="180"></a><br><sub>Edit mode</sub></td>
</tr>
</table>
## Features
**Search & Create** — Type to filter notes by title. Hit Enter on a new title to create it instantly. Search by `#tags`, `+projects`, `@contexts`, or `/folders`. Mix them freely.
**Gestures**
- Swipe right to mark a note as a favorite (swipe right again to remove)
- Swipe left to move a note to the trash
**Editor** — Three modes: View (syntax-highlighted, read-only), Edit, and Preview (rendered Markdown with LaTeX). Autosave, manual save, font size slider, and plain-text sharing from the editor menu.
**Tags** — Detected automatically from note content. `#tags`, `+projects`, and `@contexts` are all searchable.
**File Organization** — Notes live in a directory you choose. Subfolders are supported and ordered by most recently modified. Empty folders are cleaned up automatically.
**Commands** — Type in the search bar and press Enter:
| Command | Action |
|---|---|
| `:help` | Open help guide |
| `:about` | Open app info |
| `:tags` | Toggle tag visibility |
| `:fav` | Toggle favorites filter |
| `:sub` | Toggle subdirectory search |
| `:content` | Toggle file content search |
| `:day` | Switch to light theme |
| `:night` | Switch to dark theme |
| `:extern` | Toggle external editor |
| `:intern` | Toggle internal editor |
| `:emptytrash` | Permanently delete trashed notes |
## Download
Get the latest .apk on the [Release Page](https://codeberg.org/randogoth/muzzle-velocity/releases)
## Build
```sh
flutter build apk --release
```
On systems without Flutter installed, [devbox](https://www.jetify.com/devbox) provides a reproducible environment:
```sh
devbox install
devbox shell
flutter build apk --release
```
---
Made with 🧡 by randogoth

0
android/.gitignore vendored Normal file → Executable file
View file

6
assets/blaster.svg Normal file → Executable file
View file

@ -15,14 +15,14 @@
<g <g
id="g2" id="g2"
transform="matrix(-1,0,0,1,512,0)" transform="matrix(-1,0,0,1,512,0)"
style="fill:#ffffff"> style="fill:#FF9800">
<g <g
id="g1" id="g1"
style="fill:#ffffff"> style="fill:#FF9800">
<path <path
d="m 466.824,173.176 v -30.118 c 0,-12.475 -10.114,-22.588 -22.588,-22.588 -12.474,0 -22.588,10.114 -22.588,22.588 0,-12.475 -10.114,-22.588 -22.588,-22.588 -12.474,0 -22.588,10.114 -22.588,22.588 0,-12.475 -10.114,-22.588 -22.588,-22.588 -9.83,0 -18.17,6.292 -21.275,15.059 H 90.353 V 90.353 H 45.176 v 45.176 L 0,173.176 v 45.177 l 90.353,97.879 -45.177,30.121 v 75.294 h 90.353 l 43.4,-119.35 30.068,30.068 31.944,-31.944 L 196.522,256 h 136.085 c 3.105,8.767 11.445,15.059 21.275,15.059 12.475,0 22.588,-10.114 22.588,-22.588 0,12.475 10.113,22.588 22.588,22.588 12.475,0 22.588,-10.114 22.588,-22.588 0,12.475 10.113,22.588 22.588,22.588 12.475,0 22.588,-10.114 22.588,-22.588 V 218.353 H 512 V 173.176 Z M 195.765,218.353 H 90.353 v -45.177 h 105.412 z" d="m 466.824,173.176 v -30.118 c 0,-12.475 -10.114,-22.588 -22.588,-22.588 -12.474,0 -22.588,10.114 -22.588,22.588 0,-12.475 -10.114,-22.588 -22.588,-22.588 -12.474,0 -22.588,10.114 -22.588,22.588 0,-12.475 -10.114,-22.588 -22.588,-22.588 -9.83,0 -18.17,6.292 -21.275,15.059 H 90.353 V 90.353 H 45.176 v 45.176 L 0,173.176 v 45.177 l 90.353,97.879 -45.177,30.121 v 75.294 h 90.353 l 43.4,-119.35 30.068,30.068 31.944,-31.944 L 196.522,256 h 136.085 c 3.105,8.767 11.445,15.059 21.275,15.059 12.475,0 22.588,-10.114 22.588,-22.588 0,12.475 10.113,22.588 22.588,22.588 12.475,0 22.588,-10.114 22.588,-22.588 0,12.475 10.113,22.588 22.588,22.588 12.475,0 22.588,-10.114 22.588,-22.588 V 218.353 H 512 V 173.176 Z M 195.765,218.353 H 90.353 v -45.177 h 105.412 z"
id="path1" id="path1"
style="fill:#ffffff" /> style="fill:#FF9800" />
</g> </g>
</g> </g>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Before After
Before After

View file

@ -43,7 +43,9 @@ class _WrappedCodeFieldState extends State<WrappedCodeField> {
@override @override
void dispose() { void dispose() {
_focusNode.dispose(); if (widget.focusNode == null) {
_focusNode.dispose();
}
super.dispose(); super.dispose();
} }

View file

@ -14,10 +14,10 @@ import 'codefield.dart';
class NoteEditorScreen extends StatefulWidget { class NoteEditorScreen extends StatefulWidget {
final File note; final File note;
final String? notesDirectoryPath; // Add this final String? notesDirectoryPath;
final bool isDarkMode; final bool isDarkMode;
final NoteMode initialMode; final NoteMode initialMode;
final bool scrollToEnd;
const NoteEditorScreen({ const NoteEditorScreen({
super.key, super.key,
@ -25,6 +25,7 @@ class NoteEditorScreen extends StatefulWidget {
required this.isDarkMode, required this.isDarkMode,
required this.notesDirectoryPath, required this.notesDirectoryPath,
this.initialMode = NoteMode.view, this.initialMode = NoteMode.view,
this.scrollToEnd = false,
}); });
@override @override
@ -38,6 +39,7 @@ class _NoteEditorScreenState extends State<NoteEditorScreen> {
late String _initialContent; late String _initialContent;
late NoteMode _mode; late NoteMode _mode;
final FocusNode _editorFocusNode = FocusNode(); final FocusNode _editorFocusNode = FocusNode();
final ScrollController _scrollController = ScrollController();
bool _isLoading = true; bool _isLoading = true;
bool _autoSaveEnabled = false; bool _autoSaveEnabled = false;
double _fontSize = 16.0; double _fontSize = 16.0;
@ -54,6 +56,7 @@ class _NoteEditorScreenState extends State<NoteEditorScreen> {
final prefs = await SharedPreferences.getInstance(); final prefs = await SharedPreferences.getInstance();
setState(() { setState(() {
_autoSaveEnabled = prefs.getBool('auto_save_enabled') ?? false; _autoSaveEnabled = prefs.getBool('auto_save_enabled') ?? false;
_fontSize = prefs.getDouble('font_size') ?? 16.0;
}); });
} }
@ -82,12 +85,21 @@ class _NoteEditorScreenState extends State<NoteEditorScreen> {
_isLoading = false; _isLoading = false;
}); });
if (widget.initialMode == NoteMode.edit) { WidgetsBinding.instance.addPostFrameCallback((_) {
WidgetsBinding.instance.addPostFrameCallback((_) { if (widget.initialMode == NoteMode.edit) {
_controller.selection = _controller.selection =
TextSelection.collapsed(offset: _controller.text.length); TextSelection.collapsed(offset: _controller.text.length);
_editorFocusNode.requestFocus(); _editorFocusNode.requestFocus();
}); } else if (widget.scrollToEnd) {
// Two frames: first renders content, second has a stable maxScrollExtent.
WidgetsBinding.instance.addPostFrameCallback((_) => _scrollToBottom());
}
});
}
void _scrollToBottom() {
if (_scrollController.hasClients) {
_scrollController.jumpTo(_scrollController.position.maxScrollExtent);
} }
} }
@ -153,6 +165,7 @@ class _NoteEditorScreenState extends State<NoteEditorScreen> {
setState(() { setState(() {
_fontSize = newSize; _fontSize = newSize;
}); });
SharedPreferences.getInstance().then((prefs) => prefs.setDouble('font_size', newSize));
} }
@override @override
@ -160,6 +173,7 @@ class _NoteEditorScreenState extends State<NoteEditorScreen> {
_controller.removeListener(_onTextChanged); _controller.removeListener(_onTextChanged);
_controller.dispose(); _controller.dispose();
_editorFocusNode.dispose(); _editorFocusNode.dispose();
_scrollController.dispose();
super.dispose(); super.dispose();
} }
@ -208,6 +222,12 @@ class _NoteEditorScreenState extends State<NoteEditorScreen> {
break; break;
case 'Mode Edit': case 'Mode Edit':
setState(() { _mode = NoteMode.edit; }); setState(() { _mode = NoteMode.edit; });
WidgetsBinding.instance.addPostFrameCallback((_) {
_controller.selection = TextSelection.collapsed(
offset: _controller.text.length);
_editorFocusNode.requestFocus();
_scrollToBottom();
});
break; break;
case 'Mode Preview': case 'Mode Preview':
setState(() { _mode = NoteMode.preview; }); setState(() { _mode = NoteMode.preview; });
@ -362,6 +382,7 @@ class _NoteEditorScreenState extends State<NoteEditorScreen> {
data: CodeThemeData( data: CodeThemeData(
styles: widget.isDarkMode ? nightTheme : dayTheme), styles: widget.isDarkMode ? nightTheme : dayTheme),
child: SingleChildScrollView( child: SingleChildScrollView(
controller: _scrollController,
child: WrappedCodeField( child: WrappedCodeField(
wrap: true, wrap: true,
controller: _controller, controller: _controller,

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();
}); });
} }
@ -642,6 +669,16 @@ Happy note taking! ✨
case ':intern': case ':intern':
_toggleExternalEditor(false); _toggleExternalEditor(false);
break; break;
case ':fav':
setState(() { _showOnlyFavorites = !_showOnlyFavorites; });
_filterNotes();
break;
case ':sub':
_toggleIncludeSubdirectories(!_includeSubdirectories);
break;
case ':content':
_toggleIncludeFileContent(!_includeFileContent);
break;
case ':emptytrash': case ':emptytrash':
_confirmEmptyTrash(); _confirmEmptyTrash();
break; break;
@ -717,6 +754,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 +785,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 +921,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
@ -943,6 +1014,20 @@ Happy note taking! ✨
setState(() {}); // Force UI refresh setState(() {}); // Force UI refresh
} }
}, },
onLongPress: () async {
await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => NoteEditorScreen(
note: noteFile,
isDarkMode: widget.isDarkMode,
notesDirectoryPath: notesDirectoryPath,
scrollToEnd: true,
),
),
);
if (context.mounted) _loadNotes();
},
), ),
); );
}, },

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

@ -3,76 +3,97 @@ 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.
:day - Switch to light theme.
:night - Switch to dark theme.
:extern Toggle external editor usage.
:intern Toggle internal editor usage.
:emptytrash Permanently delete trashed notes.
:about Open app information. :about Open app information.
:tags Toggle tag visibility.
:fav Toggle favorites filter.
:sub Toggle subdirectory search.
:content Toggle file content search.
:day Switch to light theme.
:night Switch to dark theme.
:extern Toggle external editor.
:intern Toggle internal editor.
:emptytrash Permanently delete trashed notes.
"""; """;
String aboutContent = """ String aboutContent = """
**Muzzle Velocity** is a fast and minimalist note-taking app inspired by *Notational Velocity* and *Terminal Velocity*. v.1.1.1
**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 fast and minimalist but opinionated note-taking app inspired by Notational Velocity and Terminal Velocity."
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.1+3
environment: environment:
sdk: ^3.6.0 sdk: ^3.6.0

BIN
screenshots/01.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

BIN
screenshots/02.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

BIN
screenshots/03.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

BIN
screenshots/04.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

BIN
screenshots/05.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB