init
This commit is contained in:
commit
84c134adc4
131 changed files with 5489 additions and 0 deletions
292
lib/main.dart
Normal file
292
lib/main.dart
Normal file
|
|
@ -0,0 +1,292 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'dart:io';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'note_editor.dart';
|
||||
import 'search_service.dart';
|
||||
|
||||
void main() {
|
||||
runApp(MuzzleVelocityApp());
|
||||
}
|
||||
|
||||
class MuzzleVelocityApp extends StatefulWidget {
|
||||
@override
|
||||
_MuzzleVelocityAppState createState() => _MuzzleVelocityAppState();
|
||||
}
|
||||
|
||||
class _MuzzleVelocityAppState extends State<MuzzleVelocityApp> {
|
||||
ThemeMode _themeMode = ThemeMode.system;
|
||||
bool showTags = true;
|
||||
|
||||
void _toggleTheme(bool darkMode) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_themeMode = darkMode ? ThemeMode.dark : ThemeMode.light;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _toggleTags() {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
showTags = !showTags;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _emptyTrash() async {
|
||||
final dir = await getApplicationDocumentsDirectory();
|
||||
final trashDir = Directory('${dir.path}/notes/trash');
|
||||
if (trashDir.existsSync()) {
|
||||
trashDir.deleteSync(recursive: true);
|
||||
trashDir.createSync();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
title: 'Muzzle Velocity',
|
||||
theme: ThemeData(
|
||||
brightness: Brightness.light,
|
||||
textTheme: GoogleFonts.ibmPlexMonoTextTheme(),
|
||||
),
|
||||
darkTheme: ThemeData(
|
||||
brightness: Brightness.dark,
|
||||
textTheme: GoogleFonts.ibmPlexMonoTextTheme(),
|
||||
),
|
||||
themeMode: _themeMode,
|
||||
home: NoteListScreen(
|
||||
toggleTheme: _toggleTheme,
|
||||
toggleTags: _toggleTags,
|
||||
emptyTrash: _emptyTrash,
|
||||
isDarkMode: _themeMode == ThemeMode.dark,
|
||||
showTags: showTags,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class NoteListScreen extends StatefulWidget {
|
||||
final void Function(bool) toggleTheme;
|
||||
final VoidCallback toggleTags;
|
||||
final VoidCallback emptyTrash;
|
||||
final bool isDarkMode;
|
||||
final bool showTags;
|
||||
|
||||
NoteListScreen({
|
||||
required this.toggleTheme,
|
||||
required this.toggleTags,
|
||||
required this.emptyTrash,
|
||||
required this.isDarkMode,
|
||||
required this.showTags,
|
||||
});
|
||||
|
||||
@override
|
||||
_NoteListScreenState createState() => _NoteListScreenState();
|
||||
}
|
||||
|
||||
class _NoteListScreenState extends State<NoteListScreen> {
|
||||
TextEditingController searchController = TextEditingController();
|
||||
List<File> notes = [];
|
||||
List<File> filteredNotes = [];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadNotes();
|
||||
searchController.addListener(() {
|
||||
final text = searchController.text.trim();
|
||||
if (text.startsWith(':')) {
|
||||
_handleCommand(text);
|
||||
} else {
|
||||
_filterNotes();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _handleCommand(String text) {
|
||||
switch (text) {
|
||||
case ':tags':
|
||||
widget.toggleTags();
|
||||
break;
|
||||
case ':notags':
|
||||
widget.toggleTags();
|
||||
break;
|
||||
case ':day':
|
||||
widget.toggleTheme(false);
|
||||
break;
|
||||
case ':night':
|
||||
widget.toggleTheme(true);
|
||||
break;
|
||||
case ':emptytrash':
|
||||
_confirmEmptyTrash();
|
||||
break;
|
||||
default:
|
||||
return; // Do nothing for unrecognized commands
|
||||
}
|
||||
searchController.clear();
|
||||
}
|
||||
|
||||
void _confirmEmptyTrash() {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return AlertDialog(
|
||||
title: Text("Empty Trash"),
|
||||
content: Text("Are you sure you want to permanently delete all trashed notes?"),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: Text("Cancel"),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
widget.emptyTrash();
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: Text("Empty Trash"),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Future<void> _loadNotes() async {
|
||||
final dir = await getApplicationDocumentsDirectory();
|
||||
final notesDir = Directory('${dir.path}/notes');
|
||||
if (!notesDir.existsSync()) {
|
||||
notesDir.createSync(recursive: true);
|
||||
}
|
||||
final files = notesDir.listSync().whereType<File>().where((file) => file.path.endsWith('.md')).toList();
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
notes = files;
|
||||
filteredNotes = files;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _filterNotes() {
|
||||
final query = searchController.text.toLowerCase();
|
||||
setState(() {
|
||||
filteredNotes = notes.where((note) {
|
||||
final content = note.readAsStringSync().toLowerCase();
|
||||
return note.uri.pathSegments.last.toLowerCase().contains(query) || content.contains(query);
|
||||
}).toList();
|
||||
});
|
||||
}
|
||||
|
||||
void _deleteNote(File note) {
|
||||
final trashDir = Directory('${note.parent.path}/trash');
|
||||
if (!trashDir.existsSync()) {
|
||||
trashDir.createSync(recursive: true);
|
||||
}
|
||||
final trashedNote = File('${trashDir.path}/${note.uri.pathSegments.last}');
|
||||
note.renameSync(trashedNote.path);
|
||||
|
||||
setState(() {
|
||||
notes.remove(note);
|
||||
filteredNotes.remove(note);
|
||||
});
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Note moved to trash'),
|
||||
action: SnackBarAction(
|
||||
label: 'Undo',
|
||||
onPressed: () {
|
||||
trashedNote.renameSync(note.path);
|
||||
setState(() {
|
||||
notes.add(note);
|
||||
filteredNotes.add(note);
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<InlineSpan> _formatTags(String content) {
|
||||
final tagPattern = RegExp(r'([#@+][a-zA-Z0-9_]+)');
|
||||
final List<InlineSpan> spans = [];
|
||||
for (final match in tagPattern.allMatches(content)) {
|
||||
final tag = match.group(0)!;
|
||||
Color tagColor = Colors.white;
|
||||
if (tag.startsWith('#')) tagColor = Colors.pinkAccent;
|
||||
if (tag.startsWith('+')) tagColor = Colors.cyan;
|
||||
if (tag.startsWith('@')) tagColor = Colors.yellow;
|
||||
|
||||
spans.add(TextSpan(text: tag + ' ', style: TextStyle(color: tagColor, fontWeight: FontWeight.bold)));
|
||||
}
|
||||
return spans;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: TextField(
|
||||
enableInteractiveSelection: true,
|
||||
autocorrect: false,
|
||||
textInputAction: TextInputAction.done,
|
||||
controller: searchController,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Search or enter a command...',
|
||||
border: InputBorder.none,
|
||||
),
|
||||
style: GoogleFonts.ibmPlexMono(color: Colors.grey[300]),
|
||||
),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: Icon(widget.showTags ? Icons.tag : Icons.circle_outlined),
|
||||
onPressed: widget.toggleTags,
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(Icons.brightness_6),
|
||||
onPressed: () => widget.toggleTheme(!widget.isDarkMode),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: ListView.builder(
|
||||
itemCount: filteredNotes.length,
|
||||
itemBuilder: (context, index) {
|
||||
final noteFile = filteredNotes[index];
|
||||
final noteContent = noteFile.readAsStringSync();
|
||||
final tagSpans = _formatTags(noteContent).isNotEmpty ? _formatTags(noteContent) : [TextSpan(text: '')];
|
||||
|
||||
return Dismissible(
|
||||
key: Key(noteFile.path),
|
||||
direction: DismissDirection.endToStart,
|
||||
background: Container(
|
||||
color: Colors.redAccent,
|
||||
alignment: Alignment.centerRight,
|
||||
padding: EdgeInsets.symmetric(horizontal: 20),
|
||||
child: Icon(Icons.delete, color: Colors.white),
|
||||
),
|
||||
onDismissed: (direction) => _deleteNote(noteFile),
|
||||
child: ListTile(
|
||||
title: Text(noteFile.uri.pathSegments.last.replaceAll('.md', '')),
|
||||
subtitle: widget.showTags && tagSpans.isNotEmpty
|
||||
? RichText(text: TextSpan(style: DefaultTextStyle.of(context).style, children: tagSpans))
|
||||
: null,
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => NoteEditorScreen(
|
||||
note: noteFile,
|
||||
isDarkMode: widget.isDarkMode,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
73
lib/note_editor.dart
Normal file
73
lib/note_editor.dart
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_code_editor/flutter_code_editor.dart';
|
||||
import 'package:flutter_highlight/themes/monokai-sublime.dart';
|
||||
import 'package:highlight/languages/markdown.dart';
|
||||
import 'dart:io';
|
||||
|
||||
class NoteEditorScreen extends StatefulWidget {
|
||||
final File note;
|
||||
final bool isDarkMode;
|
||||
|
||||
NoteEditorScreen({required this.note, required this.isDarkMode});
|
||||
|
||||
@override
|
||||
_NoteEditorScreenState createState() => _NoteEditorScreenState();
|
||||
}
|
||||
|
||||
class _NoteEditorScreenState extends State<NoteEditorScreen> {
|
||||
late CodeController _controller;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadNoteContent();
|
||||
}
|
||||
|
||||
Future<void> _loadNoteContent() async {
|
||||
final content = await widget.note.readAsString();
|
||||
setState(() {
|
||||
_controller = CodeController(
|
||||
text: content,
|
||||
language: markdown,
|
||||
);
|
||||
_controller.addListener(_saveNote);
|
||||
_controller.popupController.enabled = false;
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _saveNote() async {
|
||||
await widget.note.writeAsString(_controller.text);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.removeListener(_saveNote);
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(widget.note.uri.pathSegments.last.replaceAll('.md', '')),
|
||||
),
|
||||
body: _controller == null
|
||||
? Center(child: CircularProgressIndicator())
|
||||
: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: CodeTheme(
|
||||
data: CodeThemeData(styles: monokaiSublimeTheme),
|
||||
child: CodeField(
|
||||
gutterStyle: GutterStyle.none,
|
||||
controller: _controller,
|
||||
expands: true,
|
||||
textStyle: TextStyle(
|
||||
fontSize: 14.0,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
21
lib/search_service.dart
Normal file
21
lib/search_service.dart
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import 'dart:io';
|
||||
|
||||
class SearchService {
|
||||
static List<File> searchNotes(String query, List<File> notes) {
|
||||
if (query.isEmpty) return notes;
|
||||
|
||||
final lowerQuery = query.toLowerCase();
|
||||
return notes.where((note) {
|
||||
final fileName = note.uri.pathSegments.last.replaceAll('.md', '').toLowerCase();
|
||||
final content = note.readAsStringSync().toLowerCase();
|
||||
|
||||
// Fuzzy search: Match if all words in the query are found in either the title or content
|
||||
return _matchesFuzzy(lowerQuery, fileName) || _matchesFuzzy(lowerQuery, content);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
static bool _matchesFuzzy(String query, String text) {
|
||||
final words = query.split(RegExp(r'\s+')).where((word) => word.isNotEmpty);
|
||||
return words.every((word) => text.contains(word));
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue