custom directory

This commit is contained in:
randogoth 2025-02-21 22:02:06 +02:00
parent 185a1c62e9
commit 7e82ddcbb7
12 changed files with 396 additions and 56 deletions

View file

@ -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
},
),
);