custom directory
This commit is contained in:
parent
185a1c62e9
commit
7e82ddcbb7
12 changed files with 396 additions and 56 deletions
192
lib/main.dart
192
lib/main.dart
|
|
@ -1,11 +1,19 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:muzzlevelocity/themes/day.dart';
|
||||
import 'package:muzzlevelocity/themes/night.dart';
|
||||
import 'dart:io';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'note_editor.dart';
|
||||
import 'search_service.dart';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
|
||||
void main() {
|
||||
|
||||
void main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
await Permission.storage.request();
|
||||
runApp(MuzzleVelocityApp());
|
||||
}
|
||||
|
||||
|
|
@ -44,8 +52,9 @@ class _MuzzleVelocityAppState extends State<MuzzleVelocityApp> {
|
|||
}
|
||||
|
||||
Future<void> _emptyTrash() async {
|
||||
final dir = await getApplicationDocumentsDirectory();
|
||||
final trashDir = Directory('${dir.path}/notes/trash');
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final notesPath = prefs.getString('notes_directory') ?? (await getApplicationDocumentsDirectory()).path;
|
||||
final trashDir = Directory('$notesPath/trash');
|
||||
if (trashDir.existsSync()) {
|
||||
trashDir.deleteSync(recursive: true);
|
||||
trashDir.createSync();
|
||||
|
|
@ -60,13 +69,13 @@ class _MuzzleVelocityAppState extends State<MuzzleVelocityApp> {
|
|||
brightness: Brightness.light,
|
||||
scaffoldBackgroundColor: Color(0xffF6F4F1),
|
||||
appBarTheme: AppBarTheme(backgroundColor: Color(0xffF6F4F1)),
|
||||
textTheme: GoogleFonts.ibmPlexMonoTextTheme(),
|
||||
textTheme: GoogleFonts.ibmPlexMonoTextTheme(dayText),
|
||||
),
|
||||
darkTheme: ThemeData(
|
||||
brightness: Brightness.dark,
|
||||
scaffoldBackgroundColor: Color(0xFF121212),
|
||||
appBarTheme: AppBarTheme(backgroundColor: Color(0xFF121212)),
|
||||
textTheme: GoogleFonts.ibmPlexMonoTextTheme(),
|
||||
textTheme: GoogleFonts.ibmPlexMonoTextTheme(nightText),
|
||||
),
|
||||
themeMode: _themeMode,
|
||||
home: NoteListScreen(
|
||||
|
|
@ -103,52 +112,62 @@ class _NoteListScreenState extends State<NoteListScreen> {
|
|||
TextEditingController searchController = TextEditingController();
|
||||
List<File> notes = [];
|
||||
List<File> filteredNotes = [];
|
||||
String? notesDirectoryPath;
|
||||
String? defaultDir;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadNotes();
|
||||
_initializeApp(); // Call an async method to handle initialization
|
||||
searchController.addListener(() {
|
||||
final text = searchController.text.trim();
|
||||
|
||||
if (text.startsWith(':')) {
|
||||
_handleCommand(text);
|
||||
} else {
|
||||
_filterNotes(); // Only filter, do NOT create a note here!
|
||||
_filterNotes();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _initializeApp() async {
|
||||
final dir = await getApplicationDocumentsDirectory();
|
||||
setState(() {
|
||||
defaultDir = dir.path; // Update state with defaultDir
|
||||
});
|
||||
await _loadNotes(); // Load notes after setting defaultDir
|
||||
}
|
||||
|
||||
bool _isNewNote(String title) {
|
||||
final safeTitle = _sanitizeFilename(title);
|
||||
return !notes.any((note) => note.uri.pathSegments.last == '$safeTitle.md');
|
||||
}
|
||||
|
||||
Future<void> _createNewNote(String title) async {
|
||||
final dir = await getApplicationDocumentsDirectory();
|
||||
final notesDir = Directory('${dir.path}/notes');
|
||||
void _createNewNote(String title) async {
|
||||
try {
|
||||
final safeTitle = _sanitizeFilename(title);
|
||||
final newNote = File(path.join(notesDirectoryPath!, '$safeTitle.md'));
|
||||
|
||||
if (!notesDir.existsSync()) {
|
||||
notesDir.createSync(recursive: true);
|
||||
}
|
||||
|
||||
final safeTitle = _sanitizeFilename(title);
|
||||
final newNote = File('${notesDir.path}/$safeTitle.md');
|
||||
|
||||
if (!newNote.existsSync()) {
|
||||
newNote.writeAsStringSync('# $title\n\n'); // Pre-fill with title
|
||||
setState(() {
|
||||
notes.add(newNote);
|
||||
filteredNotes.add(newNote);
|
||||
});
|
||||
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => NoteEditorScreen(
|
||||
note: newNote,
|
||||
isDarkMode: widget.isDarkMode,
|
||||
if (!newNote.existsSync()) {
|
||||
newNote.writeAsStringSync('# $title\n\n');
|
||||
setState(() {
|
||||
notes.add(newNote);
|
||||
filteredNotes.add(newNote);
|
||||
});
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => NoteEditorScreen(
|
||||
note: newNote,
|
||||
isDarkMode: widget.isDarkMode,
|
||||
notesDirectoryPath: notesDirectoryPath,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Failed to create note: $e'),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
@ -210,20 +229,57 @@ class _NoteListScreenState extends State<NoteListScreen> {
|
|||
|
||||
|
||||
Future<void> _loadNotes() async {
|
||||
final dir = await getApplicationDocumentsDirectory();
|
||||
final notesDir = Directory('${dir.path}/notes');
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
notesDirectoryPath = prefs.getString('notes_directory') ?? defaultDir; // Use custom or default directory
|
||||
|
||||
final notesDir = Directory(notesDirectoryPath!);
|
||||
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;
|
||||
});
|
||||
}
|
||||
setState(() {
|
||||
notes = files;
|
||||
filteredNotes = files;
|
||||
});
|
||||
}
|
||||
|
||||
void _showDirectoryChoiceDialog(String defaultPath) {
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierDismissible: false, // Force user to make a choice
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: Text("Choose Notes Directory"),
|
||||
content: Text("Your notes will be saved in:\n\n📂 $defaultPath\n\nWould you like to use a different folder?"),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
// Reset to default directory
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.remove('notes_directory'); // Remove custom directory from preferences
|
||||
setState(() {
|
||||
notesDirectoryPath = defaultDir; // Reset to default directory
|
||||
});
|
||||
Navigator.pop(context); // Close dialog
|
||||
_loadNotes(); // Reload notes from the default directory
|
||||
},
|
||||
child: Text("Use Default"),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
Navigator.pop(context); // Close the current dialog
|
||||
await _selectNotesDirectory(); // Allow user to pick a folder
|
||||
},
|
||||
child: Text("Choose Folder"),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
void _filterNotes() {
|
||||
final query = searchController.text.toLowerCase();
|
||||
setState(() {
|
||||
|
|
@ -279,6 +335,38 @@ class _NoteListScreenState extends State<NoteListScreen> {
|
|||
return spans;
|
||||
}
|
||||
|
||||
Future<void> _selectNotesDirectory() async {
|
||||
String? selectedDirectory = await FilePicker.platform.getDirectoryPath();
|
||||
if (selectedDirectory != null) {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString('notes_directory', selectedDirectory); // Save custom directory
|
||||
setState(() {
|
||||
notesDirectoryPath = selectedDirectory; // Update state
|
||||
});
|
||||
_loadNotes(); // Reload notes from the new directory
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> _requestStoragePermission() async {
|
||||
if (Platform.isAndroid) {
|
||||
if (await Permission.storage.request().isGranted) {
|
||||
print("✅ Basic storage permission granted.");
|
||||
return true;
|
||||
}
|
||||
|
||||
// For Android 11+ (Scoped Storage)
|
||||
if (await Permission.manageExternalStorage.request().isGranted) {
|
||||
print("✅ Full file access granted.");
|
||||
return true;
|
||||
}
|
||||
|
||||
print("❌ Storage permission denied. Opening settings...");
|
||||
await openAppSettings(); // Open settings if denied
|
||||
return false;
|
||||
}
|
||||
return true; // No permissions needed for other platforms
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
|
|
@ -322,6 +410,9 @@ class _NoteListScreenState extends State<NoteListScreen> {
|
|||
widget.toggleTheme(!widget.isDarkMode);
|
||||
});
|
||||
break;
|
||||
case 'Select Folder':
|
||||
_showDirectoryChoiceDialog(notesDirectoryPath ?? ''); // Re-trigger popup
|
||||
break;
|
||||
case 'Empty Trash':
|
||||
_confirmEmptyTrash();
|
||||
break;
|
||||
|
|
@ -346,6 +437,13 @@ class _NoteListScreenState extends State<NoteListScreen> {
|
|||
),
|
||||
),
|
||||
),
|
||||
PopupMenuItem<String>(
|
||||
value: 'Select Folder',
|
||||
child: ListTile(
|
||||
leading: Icon(Icons.folder),
|
||||
title: Text('Set Notes Directory'),
|
||||
),
|
||||
),
|
||||
PopupMenuItem<String>(
|
||||
value: 'Empty Trash',
|
||||
child: ListTile(
|
||||
|
|
@ -386,14 +484,16 @@ class _NoteListScreenState extends State<NoteListScreen> {
|
|||
await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => NoteEditorScreen(
|
||||
note: noteFile,
|
||||
isDarkMode: widget.isDarkMode,
|
||||
),
|
||||
builder: (context) =>
|
||||
NoteEditorScreen(
|
||||
note: File(path.join(notesDirectoryPath!, noteFile.uri.pathSegments.last)),
|
||||
isDarkMode: widget.isDarkMode,
|
||||
notesDirectoryPath: notesDirectoryPath,
|
||||
),
|
||||
),
|
||||
);
|
||||
_loadNotes(); // 🔄 Reload notes after returning
|
||||
setState(() {}); // 🔄 Force UI refresh
|
||||
_loadNotes(); // Reload notes after returning
|
||||
setState(() {}); // Force UI refresh
|
||||
},
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -7,18 +7,21 @@ import 'themes/day.dart';
|
|||
import 'themes/night.dart';
|
||||
import 'themes/markdown.dart';
|
||||
import 'dart:io';
|
||||
import 'package:path/path.dart' as path;
|
||||
|
||||
class NoteEditorScreen extends StatefulWidget {
|
||||
final File note;
|
||||
final String? notesDirectoryPath; // ✅ Add this
|
||||
bool isDarkMode;
|
||||
|
||||
NoteEditorScreen({required this.note, required this.isDarkMode});
|
||||
NoteEditorScreen({required this.note, required this.isDarkMode, required this.notesDirectoryPath});
|
||||
|
||||
@override
|
||||
_NoteEditorScreenState createState() => _NoteEditorScreenState();
|
||||
}
|
||||
|
||||
class _NoteEditorScreenState extends State<NoteEditorScreen> {
|
||||
bool isLoading = true;
|
||||
bool _isPreviewMode = false; // Default: Editing mode
|
||||
late CodeController _controller;
|
||||
double _fontSize = 16.0; // Default font size
|
||||
|
|
@ -37,7 +40,13 @@ class _NoteEditorScreenState extends State<NoteEditorScreen> {
|
|||
}
|
||||
|
||||
Future<void> _loadNoteContent() async {
|
||||
final content = await widget.note.readAsString();
|
||||
String content = "";
|
||||
try {
|
||||
content = await widget.note.readAsString();
|
||||
} catch (e) {
|
||||
print("❌ Failed to read file: ${widget.note.path}");
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_controller = CodeController(
|
||||
text: content,
|
||||
|
|
@ -45,11 +54,26 @@ class _NoteEditorScreenState extends State<NoteEditorScreen> {
|
|||
);
|
||||
_controller.addListener(_saveNote);
|
||||
_controller.popupController.enabled = false;
|
||||
isLoading = false;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Future<void> _saveNote() async {
|
||||
await widget.note.writeAsString(_controller.text);
|
||||
try {
|
||||
final notesDirectory = widget.notesDirectoryPath ?? widget.note.parent.path; // ✅ Use correct directory
|
||||
final newFilePath = path.join(notesDirectory, widget.note.uri.pathSegments.last);
|
||||
|
||||
print("📝 Attempting to save note at: $newFilePath");
|
||||
final newFile = File(newFilePath);
|
||||
await newFile.writeAsString(_controller.text);
|
||||
|
||||
print("✅ Successfully saved note at: $newFilePath");
|
||||
} catch (e, stackTrace) {
|
||||
print("❌ Error saving note: ${widget.note.path}");
|
||||
print("⚠️ Exception: $e");
|
||||
print("🛠 Stack trace: $stackTrace");
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
@ -118,10 +142,12 @@ class _NoteEditorScreenState extends State<NoteEditorScreen> {
|
|||
),
|
||||
],
|
||||
),
|
||||
body: _controller == null
|
||||
? Center(child: CircularProgressIndicator())
|
||||
: _isPreviewMode
|
||||
? Padding(
|
||||
body: isLoading
|
||||
? Center(child: CircularProgressIndicator()) // Show loader while initializing
|
||||
: _controller == null
|
||||
? Center(child: CircularProgressIndicator())
|
||||
: _isPreviewMode
|
||||
? Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: SingleChildScrollView(
|
||||
child: MarkdownBody(
|
||||
|
|
|
|||
|
|
@ -3,6 +3,24 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/painting.dart';
|
||||
|
||||
const TextTheme dayText = TextTheme(
|
||||
titleLarge: TextStyle(color: Color(0xff333333)),
|
||||
bodyLarge: TextStyle(color: Color(0xff333333)),
|
||||
bodyMedium: TextStyle(color: Color(0xff333333)),
|
||||
bodySmall: TextStyle(color: Color(0xff333333)),
|
||||
displayLarge: TextStyle(color: Color(0xff333333)),
|
||||
displayMedium: TextStyle(color: Color(0xff333333)),
|
||||
displaySmall: TextStyle(color: Color(0xff333333)),
|
||||
headlineLarge: TextStyle(color: Color(0xff333333)),
|
||||
headlineMedium: TextStyle(color: Color(0xff333333)),
|
||||
headlineSmall: TextStyle(color: Color(0xff333333)),
|
||||
labelLarge: TextStyle(color: Color(0xff333333)),
|
||||
labelMedium: TextStyle(color: Color(0xff333333)),
|
||||
labelSmall: TextStyle(color: Color(0xff333333)),
|
||||
titleMedium: TextStyle(color: Color(0xff333333)),
|
||||
titleSmall: TextStyle(color: Color(0xff333333)),
|
||||
);
|
||||
|
||||
const dayTheme = {
|
||||
'root':
|
||||
TextStyle(backgroundColor: Color(0xffF6F4F1), color: Color(0xff333333)),
|
||||
|
|
|
|||
|
|
@ -3,6 +3,24 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/painting.dart';
|
||||
|
||||
const TextTheme nightText = TextTheme(
|
||||
titleLarge: TextStyle(color: Color(0xffc5c8c6)),
|
||||
bodyLarge: TextStyle(color: Color(0xffc5c8c6)),
|
||||
bodyMedium: TextStyle(color: Color(0xffc5c8c6)),
|
||||
bodySmall: TextStyle(color: Color(0xffc5c8c6)),
|
||||
displayLarge: TextStyle(color: Color(0xffc5c8c6)),
|
||||
displayMedium: TextStyle(color: Color(0xffc5c8c6)),
|
||||
displaySmall: TextStyle(color: Color(0xffc5c8c6)),
|
||||
headlineLarge: TextStyle(color: Color(0xffc5c8c6)),
|
||||
headlineMedium: TextStyle(color: Color(0xffc5c8c6)),
|
||||
headlineSmall: TextStyle(color: Color(0xffc5c8c6)),
|
||||
labelLarge: TextStyle(color: Color(0xffc5c8c6)),
|
||||
labelMedium: TextStyle(color: Color(0xffc5c8c6)),
|
||||
labelSmall: TextStyle(color: Color(0xffc5c8c6)),
|
||||
titleMedium: TextStyle(color: Color(0xffc5c8c6)),
|
||||
titleSmall: TextStyle(color: Color(0xffc5c8c6)),
|
||||
);
|
||||
|
||||
const nightTheme = {
|
||||
'comment': TextStyle(color: Color(0xff969896)),
|
||||
'quote': TextStyle(color: Color(0xff969896)),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue