devbox setup, dep upgrades, lint fixes, Android build fixes
- Add devbox.json pinning Flutter 3.35.7, Android SDK (Nix flake), openjdk17, just, openssl - Add android/nix-android-sdk/ Nix flake + flake.lock + Android Studio config script - Add justfile with build/get/upgrade/icons/analyze/rebuild recipes - flutter pub upgrade --major-versions: file_picker ^11, google_fonts ^8, permission_handler ^12, share_plus ^12, flutter_lints ^6 (+71 transitive) - Fix file_picker v11 API: FilePicker.platform → FilePicker.getDirectoryPath() - Fix all 37 analyzer issues: deprecated APIs (window, WillPopScope, Share, SvgPicture color), async BuildContext guards, key constructors, private createState() return types, unused locals, print → debugPrint, etc. - Hook _handleCommand into onSubmitted for : prefix commands - Android: Gradle 8.3→8.11.1, AGP 8.2.2→8.9.1, KGP 1.8.22→2.2.20, compileSdk/targetSdk 34→36, namespace auto-derivation for library modules, kotlin.compiler.execution.strategy=in-process Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
9dbd638fa8
commit
fc6c69db24
19 changed files with 1287 additions and 383 deletions
2
lib/codefield.dart
Normal file → Executable file
2
lib/codefield.dart
Normal file → Executable file
|
|
@ -24,7 +24,7 @@ class WrappedCodeField extends StatefulWidget {
|
|||
});
|
||||
|
||||
@override
|
||||
_WrappedCodeFieldState createState() => _WrappedCodeFieldState();
|
||||
State<WrappedCodeField> createState() => _WrappedCodeFieldState();
|
||||
}
|
||||
|
||||
class _WrappedCodeFieldState extends State<WrappedCodeField> {
|
||||
|
|
|
|||
8
lib/main.dart
Normal file → Executable file
8
lib/main.dart
Normal file → Executable file
|
|
@ -14,12 +14,14 @@ void main() async {
|
|||
final license = await rootBundle.loadString('google_fonts/OFL.txt');
|
||||
yield LicenseEntryWithLineBreaks(['google_fonts'], license);
|
||||
});
|
||||
runApp(MuzzleVelocityApp());
|
||||
runApp(const MuzzleVelocityApp());
|
||||
}
|
||||
|
||||
class MuzzleVelocityApp extends StatefulWidget {
|
||||
const MuzzleVelocityApp({super.key});
|
||||
|
||||
@override
|
||||
_MuzzleVelocityAppState createState() => _MuzzleVelocityAppState();
|
||||
State<MuzzleVelocityApp> createState() => _MuzzleVelocityAppState();
|
||||
}
|
||||
|
||||
class _MuzzleVelocityAppState extends State<MuzzleVelocityApp> {
|
||||
|
|
@ -31,7 +33,7 @@ class _MuzzleVelocityAppState extends State<MuzzleVelocityApp> {
|
|||
super.initState();
|
||||
// Ensure the correct theme mode is set on startup
|
||||
_themeMode =
|
||||
WidgetsBinding.instance.window.platformBrightness == Brightness.dark
|
||||
WidgetsBinding.instance.platformDispatcher.platformBrightness == Brightness.dark
|
||||
? ThemeMode.dark
|
||||
: ThemeMode.light;
|
||||
}
|
||||
|
|
|
|||
56
lib/note_editor.dart
Normal file → Executable file
56
lib/note_editor.dart
Normal file → Executable file
|
|
@ -6,6 +6,7 @@ import 'package:flutter_markdown_latex/flutter_markdown_latex.dart';
|
|||
import 'package:flutter_svg/svg.dart';
|
||||
import 'package:markdown/markdown.dart' as md;
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:share_plus/share_plus.dart';
|
||||
import 'themes/day.dart';
|
||||
import 'themes/night.dart';
|
||||
import 'themes/markdown.dart';
|
||||
|
|
@ -16,13 +17,15 @@ class NoteEditorScreen extends StatefulWidget {
|
|||
final String? notesDirectoryPath; // ✅ Add this
|
||||
final bool isDarkMode;
|
||||
|
||||
NoteEditorScreen(
|
||||
{required this.note,
|
||||
required this.isDarkMode,
|
||||
required this.notesDirectoryPath});
|
||||
const NoteEditorScreen({
|
||||
super.key,
|
||||
required this.note,
|
||||
required this.isDarkMode,
|
||||
required this.notesDirectoryPath,
|
||||
});
|
||||
|
||||
@override
|
||||
_NoteEditorScreenState createState() => _NoteEditorScreenState();
|
||||
State<NoteEditorScreen> createState() => _NoteEditorScreenState();
|
||||
}
|
||||
|
||||
class _NoteEditorScreenState extends State<NoteEditorScreen> {
|
||||
|
|
@ -59,7 +62,7 @@ class _NoteEditorScreenState extends State<NoteEditorScreen> {
|
|||
try {
|
||||
_initialContent = await widget.note.readAsString();
|
||||
} catch (e) {
|
||||
print("❌ Failed to read file: ${widget.note.path}");
|
||||
debugPrint("❌ Failed to read file: ${widget.note.path}");
|
||||
_initialContent = "";
|
||||
}
|
||||
|
||||
|
|
@ -86,9 +89,18 @@ class _NoteEditorScreenState extends State<NoteEditorScreen> {
|
|||
await widget.note.writeAsString(_controller.text);
|
||||
_initialContent = _controller.text; // Update initial state
|
||||
|
||||
print("✅ Note saved successfully");
|
||||
debugPrint("✅ Note saved successfully");
|
||||
} catch (e) {
|
||||
print("❌ Error saving note: ${widget.note.path}");
|
||||
debugPrint("❌ Error saving note: ${widget.note.path}");
|
||||
}
|
||||
}
|
||||
|
||||
void _shareNote() {
|
||||
if (_controller.text.isNotEmpty) {
|
||||
SharePlus.instance.share(ShareParams(
|
||||
text: _controller.text,
|
||||
subject: widget.note.uri.pathSegments.last,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -108,7 +120,7 @@ class _NoteEditorScreenState extends State<NoteEditorScreen> {
|
|||
TextButton(
|
||||
onPressed: () async {
|
||||
await _saveNote();
|
||||
Navigator.of(context).pop(true); // Save and exit
|
||||
if (context.mounted) Navigator.of(context).pop(true);
|
||||
},
|
||||
child: Text("Save"),
|
||||
),
|
||||
|
|
@ -137,8 +149,13 @@ class _NoteEditorScreenState extends State<NoteEditorScreen> {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return WillPopScope(
|
||||
onWillPop: _onWillPop,
|
||||
return PopScope(
|
||||
canPop: false,
|
||||
onPopInvokedWithResult: (bool didPop, Object? result) async {
|
||||
if (didPop) return;
|
||||
final bool shouldPop = await _onWillPop();
|
||||
if (shouldPop && context.mounted) Navigator.of(context).pop();
|
||||
},
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(widget.note.uri.pathSegments.last.replaceAll('.md', '')),
|
||||
|
|
@ -146,7 +163,7 @@ class _NoteEditorScreenState extends State<NoteEditorScreen> {
|
|||
icon: Icon(Icons.arrow_back),
|
||||
onPressed: () async {
|
||||
if (await _onWillPop()) {
|
||||
Navigator.pop(context);
|
||||
if (context.mounted) Navigator.pop(context);
|
||||
}
|
||||
},
|
||||
),
|
||||
|
|
@ -157,10 +174,13 @@ class _NoteEditorScreenState extends State<NoteEditorScreen> {
|
|||
icon: SvgPicture.asset(
|
||||
"assets/blaster.svg",
|
||||
width: 30,
|
||||
color: Colors.orange,
|
||||
colorFilter: const ColorFilter.mode(Colors.orange, BlendMode.srcIn),
|
||||
),
|
||||
onSelected: (String value) {
|
||||
switch (value) {
|
||||
case 'Share': // ✅ New case for sharing
|
||||
_shareNote();
|
||||
break;
|
||||
case 'Save':
|
||||
_saveNote();
|
||||
break;
|
||||
|
|
@ -175,6 +195,14 @@ class _NoteEditorScreenState extends State<NoteEditorScreen> {
|
|||
}
|
||||
},
|
||||
itemBuilder: (BuildContext context) => [
|
||||
PopupMenuItem<String>( // ✅ Share button
|
||||
value: 'Share',
|
||||
child: ListTile(
|
||||
leading: Icon(Icons.share_outlined),
|
||||
title: Text("Share"),
|
||||
),
|
||||
),
|
||||
PopupMenuDivider(),
|
||||
PopupMenuItem<String>(
|
||||
value: 'Save',
|
||||
child: ListTile(
|
||||
|
|
@ -193,6 +221,7 @@ class _NoteEditorScreenState extends State<NoteEditorScreen> {
|
|||
: "Enable Autosave"),
|
||||
),
|
||||
),
|
||||
PopupMenuDivider(),
|
||||
PopupMenuItem<String>(
|
||||
enabled: false,
|
||||
child: Column(
|
||||
|
|
@ -219,6 +248,7 @@ class _NoteEditorScreenState extends State<NoteEditorScreen> {
|
|||
],
|
||||
),
|
||||
),
|
||||
PopupMenuDivider(),
|
||||
PopupMenuItem<String>(
|
||||
value: 'Toggle Preview',
|
||||
child: ListTile(
|
||||
|
|
|
|||
375
lib/note_list.dart
Normal file → Executable file
375
lib/note_list.dart
Normal file → Executable file
|
|
@ -19,7 +19,8 @@ class NoteListScreen extends StatefulWidget {
|
|||
final bool isDarkMode;
|
||||
final bool showTags;
|
||||
|
||||
NoteListScreen({
|
||||
const NoteListScreen({
|
||||
super.key,
|
||||
required this.toggleTheme,
|
||||
required this.toggleTags,
|
||||
required this.isDarkMode,
|
||||
|
|
@ -27,11 +28,12 @@ class NoteListScreen extends StatefulWidget {
|
|||
});
|
||||
|
||||
@override
|
||||
_NoteListScreenState createState() => _NoteListScreenState();
|
||||
State<NoteListScreen> createState() => _NoteListScreenState();
|
||||
}
|
||||
|
||||
class _NoteListScreenState extends State<NoteListScreen> {
|
||||
TextEditingController searchController = TextEditingController();
|
||||
final FocusNode searchFocusNode = FocusNode();
|
||||
List<File> notes = [];
|
||||
List<File> filteredNotes = [];
|
||||
List<File> _visibleNotes = [];
|
||||
|
|
@ -52,6 +54,9 @@ class _NoteListScreenState extends State<NoteListScreen> {
|
|||
searchController.addListener(() {
|
||||
_debouncedFilterNotes();
|
||||
});
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
FocusScope.of(context).requestFocus(searchFocusNode);
|
||||
});
|
||||
}
|
||||
|
||||
void _debouncedFilterNotes() {
|
||||
|
|
@ -95,16 +100,16 @@ class _NoteListScreenState extends State<NoteListScreen> {
|
|||
Future<void> _requestStoragePermission() async {
|
||||
if (Platform.isAndroid) {
|
||||
if (await Permission.storage.request().isGranted) {
|
||||
print("✅ Basic storage permission granted.");
|
||||
debugPrint("✅ Basic storage permission granted.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (await Permission.manageExternalStorage.request().isGranted) {
|
||||
print("✅ Full file access granted.");
|
||||
debugPrint("✅ Full file access granted.");
|
||||
return;
|
||||
}
|
||||
|
||||
print("❌ Storage permission denied. Opening settings...");
|
||||
debugPrint("❌ Storage permission denied. Opening settings...");
|
||||
await openAppSettings(); // Open settings if denied
|
||||
}
|
||||
}
|
||||
|
|
@ -116,12 +121,46 @@ class _NoteListScreenState extends State<NoteListScreen> {
|
|||
setState(() {
|
||||
defaultDir = dir.path;
|
||||
_useExternalEditor = prefs.getBool('use_external_editor') ?? false;
|
||||
_includeSubdirectories = prefs.getBool('include_subdirectories') ?? true; // ✅ Load setting
|
||||
_includeSubdirectories = prefs.getBool('include_subdirectories') ?? true;
|
||||
});
|
||||
|
||||
notesDirectoryPath = prefs.getString('notes_directory') ?? defaultDir;
|
||||
|
||||
// 🚀 Check if it's the first start OR if the directory is empty
|
||||
final directory = Directory(notesDirectoryPath!);
|
||||
if (prefs.getBool('initialized') == null ||
|
||||
!directory.listSync().any((entity) => entity is File && entity.path.endsWith('.md'))) {
|
||||
await _createDefaultNotes();
|
||||
await prefs.setBool('initialized', true);
|
||||
}
|
||||
|
||||
await _loadNotes();
|
||||
}
|
||||
|
||||
Future<void> _createDefaultNotes() async {
|
||||
final quickStartNote = File(path.join(notesDirectoryPath!, "QuickStart.md"));
|
||||
|
||||
if (!quickStartNote.existsSync()) {
|
||||
quickStartNote.writeAsStringSync("""
|
||||
# Quick Start Guide
|
||||
|
||||
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.
|
||||
|
||||
*Muzzle Velocity* is built for speed and simplicity! 🚀
|
||||
|
||||
Happy note taking! ✨
|
||||
""");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void _toggleIncludeSubdirectories(bool value) {
|
||||
setState(() {
|
||||
_includeSubdirectories = value;
|
||||
|
|
@ -157,7 +196,7 @@ class _NoteListScreenState extends State<NoteListScreen> {
|
|||
setState(() {
|
||||
notesDirectoryPath = defaultDir; // Reset to default directory
|
||||
});
|
||||
Navigator.pop(context); // Close dialog
|
||||
if (context.mounted) Navigator.pop(context);
|
||||
_loadNotes(); // Reload notes from the default directory
|
||||
},
|
||||
child: Text("Use Default"),
|
||||
|
|
@ -165,7 +204,7 @@ class _NoteListScreenState extends State<NoteListScreen> {
|
|||
TextButton(
|
||||
onPressed: () async {
|
||||
await _requestStoragePermission();
|
||||
Navigator.pop(context); // Close the current dialog
|
||||
if (context.mounted) Navigator.pop(context);
|
||||
await _selectNotesDirectory(); // Allow user to pick a folder
|
||||
},
|
||||
child: Text("Choose Folder"),
|
||||
|
|
@ -177,7 +216,7 @@ class _NoteListScreenState extends State<NoteListScreen> {
|
|||
}
|
||||
|
||||
Future<void> _selectNotesDirectory() async {
|
||||
String? selectedDirectory = await FilePicker.platform.getDirectoryPath();
|
||||
String? selectedDirectory = await FilePicker.getDirectoryPath();
|
||||
if (selectedDirectory != null) {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString(
|
||||
|
|
@ -206,7 +245,7 @@ class _NoteListScreenState extends State<NoteListScreen> {
|
|||
final entries = dir.listSync(recursive: _includeSubdirectories);
|
||||
|
||||
for (var entry in entries) {
|
||||
if (entry is File && entry.path.endsWith('.md')) {
|
||||
if (entry is File && _isValidNoteFile(entry)) {
|
||||
|
||||
if (path.basename(path.dirname(entry.path)) == "trash") {
|
||||
continue;
|
||||
|
|
@ -268,6 +307,11 @@ class _NoteListScreenState extends State<NoteListScreen> {
|
|||
return sortedTags.toSet(); // Convert back to a set after sorting
|
||||
}
|
||||
|
||||
bool _isValidNoteFile(File file) {
|
||||
final validExtensions = ['.md', '.txt', '.markdown'];
|
||||
return validExtensions.any((ext) => file.path.toLowerCase().endsWith(ext));
|
||||
}
|
||||
|
||||
void _filterNotes() {
|
||||
final query = searchController.text.trim().toLowerCase();
|
||||
|
||||
|
|
@ -351,6 +395,15 @@ class _NoteListScreenState extends State<NoteListScreen> {
|
|||
String notePath = title.trim();
|
||||
String? subfolder;
|
||||
String safeTitle;
|
||||
String extension = '.md'; // Default extension
|
||||
|
||||
// ✅ Check if user specified an extension manually
|
||||
if (title.contains('.')) {
|
||||
final ext = path.extension(title);
|
||||
if (['.txt', '.markdown', '.md'].contains(ext)) {
|
||||
extension = ext;
|
||||
}
|
||||
}
|
||||
|
||||
if (notePath.startsWith('/')) {
|
||||
final parts = notePath.split('/');
|
||||
|
|
@ -358,21 +411,21 @@ class _NoteListScreenState extends State<NoteListScreen> {
|
|||
if (parts.length > 1) {
|
||||
subfolder = parts.sublist(0, parts.length - 1).join('/');
|
||||
}
|
||||
safeTitle = _sanitizeFilename(parts.last);
|
||||
safeTitle = _sanitizeFilename(parts.last.replaceAll(extension, '')); // Remove extension from input
|
||||
} else {
|
||||
safeTitle = _sanitizeFilename(title);
|
||||
safeTitle = _sanitizeFilename(title.replaceAll(extension, ''));
|
||||
}
|
||||
|
||||
// Construct the final file path
|
||||
String fullPath = subfolder != null
|
||||
? path.join(notesDirectoryPath!, subfolder, '$safeTitle.md')
|
||||
: path.join(notesDirectoryPath!, '$safeTitle.md');
|
||||
? path.join(notesDirectoryPath!, subfolder, '$safeTitle$extension')
|
||||
: path.join(notesDirectoryPath!, '$safeTitle$extension');
|
||||
|
||||
final newNote = File(fullPath);
|
||||
|
||||
// Prevent duplicate note creation
|
||||
if (notes.any((note) => note.path == newNote.path)) {
|
||||
print("❌ Note already exists at: $fullPath");
|
||||
debugPrint("❌ Note already exists at: $fullPath");
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -416,7 +469,7 @@ class _NoteListScreenState extends State<NoteListScreen> {
|
|||
|
||||
if (notePath.startsWith('/')) {
|
||||
final parts = notePath.split('/');
|
||||
parts.removeWhere((element) => element.isEmpty); // Remove empty elements
|
||||
parts.removeWhere((element) => element.isEmpty);
|
||||
if (parts.length > 1) {
|
||||
subfolder = parts.sublist(0, parts.length - 1).join('/');
|
||||
}
|
||||
|
|
@ -425,12 +478,18 @@ class _NoteListScreenState extends State<NoteListScreen> {
|
|||
safeTitle = _sanitizeFilename(title);
|
||||
}
|
||||
|
||||
// Construct the expected file path
|
||||
String fullPath = subfolder != null
|
||||
? path.join(notesDirectoryPath!, subfolder, '$safeTitle.md')
|
||||
: path.join(notesDirectoryPath!, '$safeTitle.md');
|
||||
// ✅ Allow multiple file extensions
|
||||
for (var ext in ['.md', '.txt', '.markdown']) {
|
||||
String fullPath = subfolder != null
|
||||
? path.join(notesDirectoryPath!, subfolder, '$safeTitle$ext')
|
||||
: path.join(notesDirectoryPath!, '$safeTitle$ext');
|
||||
|
||||
return !notes.any((note) => note.path == fullPath);
|
||||
if (notes.any((note) => note.path == fullPath)) {
|
||||
return false; // A note with this name already exists
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
String _sanitizeFilename(String title) {
|
||||
|
|
@ -508,7 +567,7 @@ class _NoteListScreenState extends State<NoteListScreen> {
|
|||
_deleteEmptyParentFolders(folder.parent); // Recursively check parent
|
||||
}
|
||||
} catch (e) {
|
||||
print("❌ Error deleting empty folder: $e");
|
||||
debugPrint("❌ Error deleting empty folder: $e");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -577,13 +636,14 @@ class _NoteListScreenState extends State<NoteListScreen> {
|
|||
break;
|
||||
case ':help':
|
||||
_showInfoPage(context, "Help", helpContent);
|
||||
break;
|
||||
default:
|
||||
return; // Do nothing for unrecognized commands
|
||||
}
|
||||
searchController.clear();
|
||||
}
|
||||
|
||||
_showInfoPage(BuildContext context, String title, String content) {
|
||||
void _showInfoPage(BuildContext context, String title, String content) {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
|
|
@ -603,6 +663,7 @@ class _NoteListScreenState extends State<NoteListScreen> {
|
|||
title: Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 10),
|
||||
child: TextField(
|
||||
focusNode: searchFocusNode,
|
||||
enableInteractiveSelection: true,
|
||||
autocorrect: false,
|
||||
textInputAction: TextInputAction.done,
|
||||
|
|
@ -615,10 +676,11 @@ class _NoteListScreenState extends State<NoteListScreen> {
|
|||
style: GoogleFonts.ibmPlexMono(color: Colors.grey[300]),
|
||||
onSubmitted: (text) {
|
||||
final trimmedText = text.trim();
|
||||
if (trimmedText.isNotEmpty && _isNewNote(trimmedText)) {
|
||||
if (trimmedText.startsWith(':')) {
|
||||
_handleCommand(trimmedText);
|
||||
} else if (trimmedText.isNotEmpty && _isNewNote(trimmedText)) {
|
||||
_createNewNote(trimmedText);
|
||||
searchController
|
||||
.clear(); // Clears the search field after creation
|
||||
searchController.clear();
|
||||
}
|
||||
},
|
||||
),
|
||||
|
|
@ -626,137 +688,145 @@ class _NoteListScreenState extends State<NoteListScreen> {
|
|||
actions: [
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 15),
|
||||
child: PopupMenuButton<String>(
|
||||
icon: SvgPicture.asset(
|
||||
"assets/blaster.svg",
|
||||
width: 30,
|
||||
color: Colors.orange,
|
||||
),
|
||||
onSelected: (String value) {
|
||||
switch (value) {
|
||||
case 'Toggle Tags':
|
||||
widget.toggleTags();
|
||||
break;
|
||||
case 'Toggle Theme':
|
||||
setState(() {
|
||||
widget.toggleTheme(!widget.isDarkMode);
|
||||
});
|
||||
break;
|
||||
case 'External Editor':
|
||||
_toggleExternalEditor(!_useExternalEditor);
|
||||
break;
|
||||
case 'Select Folder':
|
||||
_showDirectoryChoiceDialog(
|
||||
notesDirectoryPath ?? ''); // Re-trigger popup
|
||||
break;
|
||||
case 'Empty Trash':
|
||||
_confirmEmptyTrash();
|
||||
break;
|
||||
case 'Help':
|
||||
_showInfoPage(context, "Help", helpContent);
|
||||
break;
|
||||
case 'About':
|
||||
_showInfoPage(context, "About", aboutContent);
|
||||
break;
|
||||
}
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
if (searchController.text.isNotEmpty) {
|
||||
searchController.clear();
|
||||
}
|
||||
},
|
||||
itemBuilder: (BuildContext context) => [
|
||||
PopupMenuItem<String>(
|
||||
value: 'Toggle Tags',
|
||||
child: ListTile(
|
||||
leading:
|
||||
Icon(widget.showTags ? Icons.short_text : Icons.tag),
|
||||
title: Text(widget.showTags ? 'Hide Tags' : 'Show Tags'),
|
||||
),
|
||||
child: PopupMenuButton<String>(
|
||||
enabled: searchController.text.isEmpty,
|
||||
icon: SvgPicture.asset(
|
||||
"assets/blaster.svg",
|
||||
width: 30,
|
||||
colorFilter: const ColorFilter.mode(Colors.orange, BlendMode.srcIn),
|
||||
),
|
||||
PopupMenuItem<String>(
|
||||
value: 'Toggle Theme',
|
||||
child: ListTile(
|
||||
leading: Icon(
|
||||
Theme.of(context).brightness == Brightness.dark
|
||||
? Icons.light_mode
|
||||
: Icons.dark_mode,
|
||||
),
|
||||
title: Text(
|
||||
Theme.of(context).brightness == Brightness.dark
|
||||
? 'Switch to Light Mode'
|
||||
: 'Switch to Dark Mode',
|
||||
onSelected: (String value) {
|
||||
switch (value) {
|
||||
case 'Toggle Tags':
|
||||
widget.toggleTags();
|
||||
break;
|
||||
case 'Toggle Theme':
|
||||
setState(() {
|
||||
widget.toggleTheme(!widget.isDarkMode);
|
||||
});
|
||||
break;
|
||||
case 'External Editor':
|
||||
_toggleExternalEditor(!_useExternalEditor);
|
||||
break;
|
||||
case 'Select Folder':
|
||||
_showDirectoryChoiceDialog(
|
||||
notesDirectoryPath ?? ''); // Re-trigger popup
|
||||
break;
|
||||
case 'Empty Trash':
|
||||
_confirmEmptyTrash();
|
||||
break;
|
||||
case 'Help':
|
||||
_showInfoPage(context, "Help", helpContent);
|
||||
break;
|
||||
case 'About':
|
||||
_showInfoPage(context, "About", aboutContent);
|
||||
break;
|
||||
}
|
||||
},
|
||||
itemBuilder: (BuildContext context) => [
|
||||
PopupMenuItem<String>(
|
||||
value: 'Toggle Tags',
|
||||
child: ListTile(
|
||||
leading:
|
||||
Icon(widget.showTags ? Icons.short_text : Icons.tag),
|
||||
title: Text(widget.showTags ? 'Hide Tags' : 'Show Tags'),
|
||||
),
|
||||
),
|
||||
),
|
||||
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: () {
|
||||
_toggleIncludeFileContent(!_includeFileContent);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
),
|
||||
PopupMenuItem<String>(
|
||||
value: 'External Editor',
|
||||
child: ListTile(
|
||||
leading: Icon(
|
||||
_useExternalEditor
|
||||
? Icons.open_in_new_off
|
||||
: Icons.open_in_new,
|
||||
),
|
||||
title: Text(
|
||||
!_useExternalEditor
|
||||
? 'Use External Editor'
|
||||
: 'Use Internal Editor',
|
||||
PopupMenuItem<String>(
|
||||
value: 'Toggle Theme',
|
||||
child: ListTile(
|
||||
leading: Icon(
|
||||
Theme.of(context).brightness == Brightness.dark
|
||||
? Icons.light_mode
|
||||
: Icons.dark_mode,
|
||||
),
|
||||
title: Text(
|
||||
Theme.of(context).brightness == Brightness.dark
|
||||
? 'Switch to Light Mode'
|
||||
: 'Switch to Dark Mode',
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
PopupMenuItem<String>(
|
||||
value: 'Select Folder',
|
||||
child: ListTile(
|
||||
leading: Icon(Icons.folder_shared_outlined),
|
||||
title: Text('Set Notes Directory'),
|
||||
PopupMenuItem<String>(
|
||||
value: 'Include Subdirectories',
|
||||
child: ListTile(
|
||||
leading: Icon(_includeSubdirectories ? Icons.file_copy_outlined : Icons.folder_copy_outlined),
|
||||
title: Text(_includeSubdirectories ? 'Exclude Subdirectories' : 'Search Subdirectories'),
|
||||
onTap: () {
|
||||
_toggleIncludeSubdirectories(!_includeSubdirectories);
|
||||
Navigator.pop(context); // Close menu after toggle
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
PopupMenuDivider(),
|
||||
PopupMenuItem<String>(
|
||||
value: 'Empty Trash',
|
||||
child: ListTile(
|
||||
leading: Icon(Icons.delete, color: Colors.redAccent),
|
||||
title: Text('Empty Trash',
|
||||
style: TextStyle(color: Colors.redAccent)),
|
||||
PopupMenuItem<String>(
|
||||
value: 'Include File Content',
|
||||
child: ListTile(
|
||||
leading: Icon(
|
||||
_includeFileContent ? Icons.highlight_off : Icons.file_open_outlined,
|
||||
),
|
||||
title: Text(
|
||||
_includeFileContent ? 'Exclude File Content' : 'Search File Content',
|
||||
),
|
||||
onTap: () {
|
||||
_toggleIncludeFileContent(!_includeFileContent);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
PopupMenuDivider(),
|
||||
PopupMenuItem<String>(
|
||||
value: 'Help',
|
||||
child: ListTile(
|
||||
leading: Icon(Icons.help_outline),
|
||||
title: Text('Help'),
|
||||
PopupMenuItem<String>(
|
||||
value: 'External Editor',
|
||||
child: ListTile(
|
||||
leading: Icon(
|
||||
_useExternalEditor
|
||||
? Icons.open_in_new_off
|
||||
: Icons.open_in_new,
|
||||
),
|
||||
title: Text(
|
||||
!_useExternalEditor
|
||||
? 'Use External Editor'
|
||||
: 'Use Internal Editor',
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
PopupMenuItem<String>(
|
||||
value: 'About',
|
||||
child: ListTile(
|
||||
leading: Icon(Icons.info_outline),
|
||||
title: Text('About'),
|
||||
PopupMenuItem<String>(
|
||||
value: 'Select Folder',
|
||||
child: ListTile(
|
||||
leading: Icon(Icons.folder_shared_outlined),
|
||||
title: Text('Set Notes Directory'),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
PopupMenuDivider(),
|
||||
PopupMenuItem<String>(
|
||||
value: 'Empty Trash',
|
||||
child: ListTile(
|
||||
leading: Icon(Icons.delete, color: Colors.redAccent),
|
||||
title: Text('Empty Trash',
|
||||
style: TextStyle(color: Colors.redAccent)),
|
||||
),
|
||||
),
|
||||
PopupMenuDivider(),
|
||||
PopupMenuItem<String>(
|
||||
value: 'Help',
|
||||
child: ListTile(
|
||||
leading: Icon(Icons.help_outline),
|
||||
title: Text('Help'),
|
||||
),
|
||||
),
|
||||
PopupMenuItem<String>(
|
||||
value: 'About',
|
||||
child: ListTile(
|
||||
leading: Icon(Icons.info_outline),
|
||||
title: Text('About'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
@ -783,11 +853,6 @@ class _NoteListScreenState extends State<NoteListScreen> {
|
|||
}
|
||||
|
||||
final noteFile = filteredNotes[index];
|
||||
final noteContent = _includeFileContent ? noteFile.readAsStringSync() : ""; // ✅ Read content only if needed
|
||||
final tagSpans = _formatTags(noteContent).isNotEmpty
|
||||
? _formatTags(noteContent)
|
||||
: [TextSpan(text: '')];
|
||||
|
||||
return Dismissible(
|
||||
key: Key(noteFile.path),
|
||||
direction: DismissDirection.endToStart,
|
||||
|
|
@ -836,7 +901,7 @@ class _NoteListScreenState extends State<NoteListScreen> {
|
|||
if (result.stdout.toString().trim().isNotEmpty) {
|
||||
await Process.run('xdg-open', [noteFile.path]);
|
||||
} else {
|
||||
print("❌ `xdg-open` is not available.");
|
||||
debugPrint("❌ `xdg-open` is not available.");
|
||||
}
|
||||
} else if (Platform.isAndroid) {
|
||||
await _requestStoragePermission();
|
||||
|
|
@ -844,10 +909,10 @@ class _NoteListScreenState extends State<NoteListScreen> {
|
|||
noteFile.path,
|
||||
type: "text/markdown",
|
||||
);
|
||||
print("✅ OpenFilex result: ${result.type}, message: ${result.message}");
|
||||
debugPrint("✅ OpenFilex result: ${result.type}, message: ${result.message}");
|
||||
}
|
||||
} catch (e) {
|
||||
print("❌ Failed to open external editor: $e");
|
||||
debugPrint("❌ Failed to open external editor: $e");
|
||||
}
|
||||
} else {
|
||||
await Navigator.push(
|
||||
|
|
|
|||
3
lib/read_only.dart
Normal file → Executable file
3
lib/read_only.dart
Normal file → Executable file
|
|
@ -10,7 +10,8 @@ class ReadOnlyPage extends StatelessWidget {
|
|||
final String content;
|
||||
final bool isDarkMode;
|
||||
|
||||
ReadOnlyPage({
|
||||
const ReadOnlyPage({
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.content,
|
||||
required this.isDarkMode,
|
||||
|
|
|
|||
273
lib/themes/markdown.dart
Normal file → Executable file
273
lib/themes/markdown.dart
Normal file → Executable file
|
|
@ -1,61 +1,218 @@
|
|||
import 'package:highlight/highlight.dart';
|
||||
|
||||
final markdown = Mode(refs: {}, aliases: [
|
||||
"md",
|
||||
"mkdown",
|
||||
"mkd"
|
||||
], contains: [
|
||||
Mode(className: "section", variants: [
|
||||
Mode(begin: "^#\\s{1,6}", end: "\$"),
|
||||
Mode(begin: "^.+?\\n[=-]{2,}\$")
|
||||
]),
|
||||
Mode(begin: "<", end: ">", subLanguage: ["xml"], relevance: 0),
|
||||
Mode(className: "bullet", begin: "^\\s*([*+-]|(\\d+\\.))\\s+"),
|
||||
Mode(className: "strong", begin: "[*_]{2}.+?[*_]{2}"),
|
||||
final markdown = Mode(
|
||||
refs: {},
|
||||
aliases: [
|
||||
"md",
|
||||
"mkdown",
|
||||
"mkd"
|
||||
],
|
||||
contains: [
|
||||
// Headers (with distinct classes for each level)
|
||||
Mode(
|
||||
className: "header",
|
||||
variants: [
|
||||
// Level 1 header (#)
|
||||
Mode(
|
||||
className: "header-h1",
|
||||
begin: "^#\\s+",
|
||||
end: "\$"
|
||||
),
|
||||
// Level 2 header (##)
|
||||
Mode(
|
||||
className: "header-h2",
|
||||
begin: "^##\\s+",
|
||||
end: "\$"
|
||||
),
|
||||
// Level 3 header (###)
|
||||
Mode(
|
||||
className: "header-h3",
|
||||
begin: "^###\\s+",
|
||||
end: "\$"
|
||||
),
|
||||
// Level 4 header (####)
|
||||
Mode(
|
||||
className: "header-h4",
|
||||
begin: "^####\\s+",
|
||||
end: "\$"
|
||||
),
|
||||
// Level 5 header (#####)
|
||||
Mode(
|
||||
className: "header-h5",
|
||||
begin: "^#####\\s+",
|
||||
end: "\$"
|
||||
),
|
||||
// Level 6 header (######)
|
||||
Mode(
|
||||
className: "header-h6",
|
||||
begin: "^######\\s+",
|
||||
end: "\$"
|
||||
),
|
||||
// Underlined headers (=== and ---)
|
||||
Mode(
|
||||
className: "header-underline",
|
||||
begin: "^.+?\\n[=-]{2,}\$"
|
||||
)
|
||||
]
|
||||
),
|
||||
|
||||
// HTML tags
|
||||
Mode(
|
||||
begin: "<",
|
||||
end: ">",
|
||||
subLanguage: ["xml"],
|
||||
relevance: 0
|
||||
),
|
||||
|
||||
// Lists
|
||||
Mode(
|
||||
className: "list",
|
||||
begin: "^\\s*([*+-]|(\\d+\\.))\\s+"
|
||||
),
|
||||
|
||||
// Bold text
|
||||
Mode(
|
||||
className: "bold",
|
||||
begin: "[*_]{2}.+?[*_]{2}"
|
||||
),
|
||||
|
||||
// Italic text
|
||||
Mode(
|
||||
className: "italic",
|
||||
variants: [
|
||||
Mode(begin: "\\*.+?\\*"),
|
||||
Mode(begin: "_.+?_", relevance: 0)
|
||||
]
|
||||
),
|
||||
|
||||
// Blockquotes
|
||||
Mode(
|
||||
className: "blockquote",
|
||||
begin: "^>\\s+",
|
||||
end: "\$"
|
||||
),
|
||||
|
||||
// Code blocks and inline code
|
||||
Mode(
|
||||
className: "code",
|
||||
variants: [
|
||||
Mode(begin: "^```\\w*\\s*\$", end: "^```[ ]*\$"), // Fenced code blocks
|
||||
Mode(begin: "`.+?`"), // Inline code
|
||||
Mode(begin: "^( {4}|\\t)", end: "\$", relevance: 0) // Indented code blocks
|
||||
]
|
||||
),
|
||||
|
||||
// Horizontal rules
|
||||
Mode(
|
||||
className: "horizontal-rule",
|
||||
begin: "^[-\\*]{3,}",
|
||||
end: "\$"
|
||||
),
|
||||
|
||||
// Links and images
|
||||
Mode(
|
||||
begin: "\\[.+?\\][\\(\\[].*?[\\)\\]]",
|
||||
returnBegin: true,
|
||||
contains: [
|
||||
Mode(
|
||||
className: "link-text",
|
||||
begin: "\\[",
|
||||
end: "\\]",
|
||||
excludeBegin: true,
|
||||
returnEnd: true,
|
||||
relevance: 0
|
||||
),
|
||||
Mode(
|
||||
className: "link-url",
|
||||
begin: "\\]\\(",
|
||||
end: "\\)",
|
||||
excludeBegin: true,
|
||||
excludeEnd: true
|
||||
),
|
||||
Mode(
|
||||
className: "link-reference",
|
||||
begin: "\\]\\[",
|
||||
end: "\\]",
|
||||
excludeBegin: true,
|
||||
excludeEnd: true
|
||||
)
|
||||
],
|
||||
relevance: 10
|
||||
),
|
||||
|
||||
// Reference-style links
|
||||
Mode(
|
||||
begin: "^\\[[^\\n]+\\]:",
|
||||
returnBegin: true,
|
||||
contains: [
|
||||
Mode(
|
||||
className: "link-reference",
|
||||
begin: "\\[",
|
||||
end: "\\]",
|
||||
excludeBegin: true,
|
||||
excludeEnd: true
|
||||
),
|
||||
Mode(
|
||||
className: "link-url",
|
||||
begin: ":\\s*",
|
||||
end: "\$",
|
||||
excludeBegin: true
|
||||
)
|
||||
]
|
||||
),
|
||||
|
||||
// Strikethrough text
|
||||
Mode(
|
||||
className: "strikethrough",
|
||||
begin: "~~.+?~~"
|
||||
),
|
||||
|
||||
// Tables
|
||||
Mode(
|
||||
className: "table",
|
||||
begin: "^\\|.+?\\|",
|
||||
end: "\$",
|
||||
contains: [
|
||||
Mode(
|
||||
className: "table-header",
|
||||
begin: "^\\|",
|
||||
end: "\\|",
|
||||
contains: [
|
||||
Mode(
|
||||
className: "table-cell",
|
||||
begin: "[^|]+",
|
||||
end: "\$"
|
||||
)
|
||||
]
|
||||
),
|
||||
Mode(
|
||||
className: "table-row",
|
||||
begin: "^\\|",
|
||||
end: "\\|",
|
||||
contains: [
|
||||
Mode(
|
||||
className: "table-cell",
|
||||
begin: "[^|]+",
|
||||
end: "\$"
|
||||
)
|
||||
]
|
||||
)
|
||||
]
|
||||
),
|
||||
|
||||
// Task lists
|
||||
Mode(
|
||||
className: "task-list",
|
||||
begin: "^\\s*-\\s*\\[\\s*[xX ]?\\s*\\]\\s+"
|
||||
),
|
||||
|
||||
Mode(
|
||||
className: "emphasis",
|
||||
variants: [Mode(begin: "\\*.+?\\*"), Mode(begin: "_.+?_", relevance: 0)]),
|
||||
Mode(className: "quote", begin: "^>\\s+", end: "\$"),
|
||||
Mode(className: "code", variants: [
|
||||
Mode(begin: "^```\\w*\\s*\$", end: "^```[ ]*\$"),
|
||||
Mode(begin: "`.+?`"),
|
||||
Mode(begin: "^( {4}|\\t)", end: "\$", relevance: 0)
|
||||
]),
|
||||
Mode(begin: "^[-\\*]{3,}", end: "\$"),
|
||||
Mode(
|
||||
begin: "\\[.+?\\][\\(\\[].*?[\\)\\]]",
|
||||
returnBegin: true,
|
||||
contains: [
|
||||
Mode(
|
||||
className: "string",
|
||||
begin: "\\[",
|
||||
end: "\\]",
|
||||
excludeBegin: true,
|
||||
returnEnd: true,
|
||||
relevance: 0),
|
||||
Mode(
|
||||
className: "link",
|
||||
begin: "\\]\\(",
|
||||
end: "\\)",
|
||||
excludeBegin: true,
|
||||
excludeEnd: true),
|
||||
Mode(
|
||||
className: "symbol",
|
||||
begin: "\\]\\[",
|
||||
end: "\\]",
|
||||
excludeBegin: true,
|
||||
excludeEnd: true)
|
||||
],
|
||||
relevance: 10),
|
||||
Mode(begin: "^\\[[^\\n]+\\]:", returnBegin: true, contains: [
|
||||
Mode(
|
||||
className: "symbol",
|
||||
begin: "\\[",
|
||||
end: "\\]",
|
||||
excludeBegin: true,
|
||||
excludeEnd: true),
|
||||
Mode(className: "link", begin: ":\\s*", end: "\$", excludeBegin: true)
|
||||
]),
|
||||
className: "link-reference",
|
||||
begin: "\\[",
|
||||
end: "\\]",
|
||||
excludeBegin: true,
|
||||
excludeEnd: true
|
||||
),
|
||||
|
||||
// Custom Tag Highlighting
|
||||
Mode(
|
||||
|
|
@ -71,11 +228,11 @@ final markdown = Mode(refs: {}, aliases: [
|
|||
begin: "(?<=\\s|^)@[a-zA-Z0-9_]+",
|
||||
relevance: 10), // Yellow
|
||||
Mode(
|
||||
className: "keyword",
|
||||
className: "command",
|
||||
begin: "(?<=\\s|^):[a-zA-Z0-9_]+",
|
||||
relevance: 10),
|
||||
Mode(
|
||||
className: "variable",
|
||||
begin: "(?<=\\s|^)\/[a-zA-Z0-9_\/]+",
|
||||
className: "link-url",
|
||||
begin: "(?<=\\s|^)/[a-zA-Z0-9_/]+",
|
||||
relevance: 10), // Yellow
|
||||
]);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue