Support multi note selection

Fixes #90
This commit is contained in:
Vishesh Handa
2021-10-14 15:25:26 +02:00
parent de4f5d0804
commit 097ffc6b42
8 changed files with 139 additions and 79 deletions

View File

@ -370,7 +370,12 @@ widgets:
actions: actions:
moveToFolder: Move To Folder moveToFolder: Move To Folder
NoteDeleteDialog: NoteDeleteDialog:
title: Do you want to delete this note? title:
one: Do you want to delete this note?
two: Do you want to delete these {} notes?
few: Do you want to delete these {} notes?
many: Do you want to delete these {} notes?
other: Do you want to delete these {} notes?
yes: Yes yes: Yes
no: No no: No
NoteEditor: NoteEditor:

View File

@ -18,8 +18,28 @@ class CommitMessageBuilder {
String moveNote(String oldSpec, String newSpec) => String moveNote(String oldSpec, String newSpec) =>
"Moved Note $oldSpec -> $newSpec"; "Moved Note $oldSpec -> $newSpec";
String moveNotes(List<String> oldSpecs, List<String> newSpecs) {
var msg = "Moved ${oldSpecs.length} Notes\n\n";
for (var i = 0; i < oldSpecs.length; i++) {
var oldSpec = oldSpecs[i];
var newSpec = newSpecs[i];
msg += '* $oldSpec -> $newSpec';
}
return msg;
}
String removeNote(String spec) => "Removed Note $spec"; String removeNote(String spec) => "Removed Note $spec";
String removeFolder(String spec) => "Removed Folder $spec"; String removeFolder(String spec) => "Removed Folder $spec";
String removeNotes(Iterable<String> specs) {
var list = specs.toList();
var msg = "Removed ${list.length} Notes\n\n";
for (var spec in list) {
msg += '- $spec';
}
return msg;
}
String updateNote(String spec) => "Updated Note $spec"; String updateNote(String spec) => "Updated Note $spec";
} }

View File

@ -163,25 +163,32 @@ class GitNoteRepository {
return _addAllAndCommit(msg); return _addAllAndCommit(msg);
} }
Future<Result<void>> moveNote( Future<Result<void>> moveNotes(
String oldFullPath, List<String> oldPaths,
String newFullPath, List<String> newPaths,
String newFolderPath,
) async { ) async {
var repoPath = gitRepoPath.endsWith('/') ? gitRepoPath : '$gitRepoPath/'; var repoPath = gitRepoPath.endsWith('/') ? gitRepoPath : '$gitRepoPath/';
var oldSpec = oldFullPath.substring(repoPath.length); var oldSpecs = oldPaths.map((p) => p.substring(repoPath.length)).toList();
var newSpec = newFullPath.substring(repoPath.length); var newSpecs = newPaths.map((p) => p.substring(repoPath.length)).toList();
var msg = messageBuilder.moveNote(oldSpec, newSpec);
var msg = oldPaths.length == 1
? messageBuilder.moveNote(oldSpecs.first, newSpecs.first)
: messageBuilder.moveNotes(oldSpecs, newSpecs);
return _addAllAndCommit(msg); return _addAllAndCommit(msg);
} }
Future<Result<void>> removeNote(Note note) async { Future<Result<void>> removeNotes(List<Note> notes) async {
return catchAll(() async { return catchAll(() async {
// We are not calling note.remove() as gitRm will also remove the file // We are not calling note.remove() as gitRm will also remove the file
var spec = note.pathSpec(); for (var note in notes) {
await _rm(spec).throwOnError(); var spec = note.pathSpec();
await _rm(spec).throwOnError();
}
await _commit( await _commit(
message: messageBuilder.removeNote(spec), message: notes.length == 1
? messageBuilder.removeNote(notes.first.pathSpec())
: messageBuilder.removeNotes(notes.map((n) => n.pathSpec())),
authorEmail: config.gitAuthorEmail, authorEmail: config.gitAuthorEmail,
authorName: config.gitAuthor, authorName: config.gitAuthor,
).throwOnError(); ).throwOnError();

View File

@ -34,7 +34,7 @@ Widget buildFolderView({
required NoteSelectedFunction noteTapped, required NoteSelectedFunction noteTapped,
required NoteSelectedFunction noteLongPressed, required NoteSelectedFunction noteLongPressed,
required NoteBoolPropertyFunction isNoteSelected, required NoteBoolPropertyFunction isNoteSelected,
required String searchTerm, String searchTerm = "",
}) { }) {
switch (viewType) { switch (viewType) {
case FolderViewType.Standard: case FolderViewType.Standard:

View File

@ -68,8 +68,8 @@ class _FolderViewState extends State<FolderView> {
var _headerType = StandardViewHeader.TitleGenerated; var _headerType = StandardViewHeader.TitleGenerated;
bool _showSummary = true; bool _showSummary = true;
Note? selectedNote; var selectedNotes = <Note>[];
bool get inSelectionMode => selectedNote != null; bool get inSelectionMode => selectedNotes.isNotEmpty;
@override @override
void initState() { void initState() {
@ -119,7 +119,7 @@ class _FolderViewState extends State<FolderView> {
Widget _buildBody(BuildContext context) { Widget _buildBody(BuildContext context) {
var title = widget.notesFolder.publicName; var title = widget.notesFolder.publicName;
if (inSelectionMode) { if (inSelectionMode) {
title = NumberFormat.compact().format(1); title = NumberFormat.compact().format(selectedNotes.length);
} }
var folderView = buildFolderView( var folderView = buildFolderView(
@ -128,20 +128,9 @@ class _FolderViewState extends State<FolderView> {
emptyText: tr(LocaleKeys.screens_folder_view_empty), emptyText: tr(LocaleKeys.screens_folder_view_empty),
header: _headerType, header: _headerType,
showSummary: _showSummary, showSummary: _showSummary,
noteTapped: (Note note) { noteTapped: _noteTapped,
if (!inSelectionMode) { noteLongPressed: _noteLongPress,
openNoteEditor(context, note, widget.notesFolder); isNoteSelected: (n) => selectedNotes.contains(n),
} else {
_resetSelection();
}
},
noteLongPressed: (Note note) {
setState(() {
selectedNote = note;
});
},
isNoteSelected: (n) => n == selectedNote,
searchTerm: "",
); );
Widget pinnedFolderView = const SizedBox(); Widget pinnedFolderView = const SizedBox();
@ -152,23 +141,11 @@ class _FolderViewState extends State<FolderView> {
emptyText: null, emptyText: null,
header: _headerType, header: _headerType,
showSummary: _showSummary, showSummary: _showSummary,
noteTapped: (Note note) { noteTapped: _noteTapped,
if (!inSelectionMode) { noteLongPressed: _noteLongPress,
openNoteEditor(context, note, widget.notesFolder); isNoteSelected: (n) => selectedNotes.contains(n),
} else {
_resetSelection();
}
},
noteLongPressed: (Note note) {
setState(() {
selectedNote = note;
});
},
isNoteSelected: (n) => n == selectedNote,
searchTerm: "",
); );
} }
// assert(folderView is SliverWithKeepAliveWidget);
var settings = Provider.of<Settings>(context); var settings = Provider.of<Settings>(context);
final showButtomMenuBar = settings.bottomMenuBar; final showButtomMenuBar = settings.bottomMenuBar;
@ -231,6 +208,37 @@ class _FolderViewState extends State<FolderView> {
); );
} }
void _noteLongPress(Note note) {
var i = selectedNotes.indexOf(note);
if (i != -1) {
setState(() {
selectedNotes.removeAt(i);
});
} else {
setState(() {
selectedNotes.add(note);
});
}
}
void _noteTapped(Note note) {
if (!inSelectionMode) {
openNoteEditor(context, note, widget.notesFolder);
return;
}
var i = selectedNotes.indexOf(note);
if (i != -1) {
setState(() {
selectedNotes.removeAt(i);
});
} else {
setState(() {
selectedNotes.add(note);
});
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var createButton = FloatingActionButton( var createButton = FloatingActionButton(
@ -558,7 +566,7 @@ class _FolderViewState extends State<FolderView> {
onSelected: (NoteSelectedExtraActions choice) { onSelected: (NoteSelectedExtraActions choice) {
switch (choice) { switch (choice) {
case NoteSelectedExtraActions.MoveToFolder: case NoteSelectedExtraActions.MoveToFolder:
_moveNoteToFolder(); _moveSelectedNotesToFolder();
break; break;
} }
}, },
@ -572,50 +580,48 @@ class _FolderViewState extends State<FolderView> {
); );
return <Widget>[ return <Widget>[
IconButton( if (selectedNotes.length == 1)
icon: const Icon(Icons.share), IconButton(
onPressed: () async { icon: const Icon(Icons.share),
await shareNote(selectedNote!); onPressed: () async {
_resetSelection(); await shareNote(selectedNotes.first);
}, _resetSelection();
), },
),
IconButton( IconButton(
icon: const Icon(Icons.delete), icon: const Icon(Icons.delete),
onPressed: _deleteNote, onPressed: _deleteSelectedNotes,
), ),
extraActions, extraActions,
]; ];
} }
void _deleteNote() async { void _deleteSelectedNotes() async {
var note = selectedNote;
var settings = Provider.of<Settings>(context, listen: false); var settings = Provider.of<Settings>(context, listen: false);
var shouldDelete = true; var shouldDelete = true;
if (settings.confirmDelete) { if (settings.confirmDelete) {
shouldDelete = await showDialog( shouldDelete = (await showDialog(
context: context, context: context,
builder: (context) => NoteDeleteDialog(), builder: (context) => NoteDeleteDialog(num: selectedNotes.length),
); )) ==
true;
} }
if (shouldDelete == true) { if (shouldDelete == true) {
var stateContainer = context.read<GitJournalRepo>(); var repo = context.read<GitJournalRepo>();
stateContainer.removeNote(note!); repo.removeNotes(selectedNotes);
} }
_resetSelection(); _resetSelection();
} }
void _moveNoteToFolder() async { void _moveSelectedNotesToFolder() async {
var note = selectedNote!;
var destFolder = await showDialog<NotesFolderFS>( var destFolder = await showDialog<NotesFolderFS>(
context: context, context: context,
builder: (context) => FolderSelectionDialog(), builder: (context) => FolderSelectionDialog(),
); );
if (destFolder != null) { if (destFolder != null) {
var repo = context.read<GitJournalRepo>(); var repo = context.read<GitJournalRepo>();
repo.moveNote(note, destFolder); repo.moveNotes(selectedNotes, destFolder);
} }
_resetSelection(); _resetSelection();
@ -623,7 +629,7 @@ class _FolderViewState extends State<FolderView> {
void _resetSelection() { void _resetSelection() {
setState(() { setState(() {
selectedNote = null; selectedNotes = [];
}); });
} }
} }

View File

@ -366,8 +366,15 @@ class GitJournalRepo with ChangeNotifier {
}); });
} }
void moveNote(Note note, NotesFolderFS destFolder) async { Future<void> moveNote(Note note, NotesFolderFS destFolder) =>
if (destFolder.folderPath == note.parent.folderPath) { moveNotes([note], destFolder);
Future<void> moveNotes(List<Note> notes, NotesFolderFS destFolder) async {
notes = notes
.where((n) => n.parent.folderPath != destFolder.folderPath)
.toList();
if (notes.isEmpty) {
return; return;
} }
@ -375,10 +382,17 @@ class GitJournalRepo with ChangeNotifier {
return _opLock.synchronized(() async { return _opLock.synchronized(() async {
Log.d("Got moveNote lock"); Log.d("Got moveNote lock");
var oldNotePath = note.filePath; var oldPaths = <String>[];
NotesFolderFS.moveNote(note, destFolder); var newPaths = <String>[];
for (var note in notes) {
oldPaths.add(note.filePath);
NotesFolderFS.moveNote(note, destFolder);
newPaths.add(note.filePath);
}
_gitRepo.moveNote(oldNotePath, note.filePath).then((Result<void> _) { _gitRepo
.moveNotes(oldPaths, newPaths, destFolder.folderPath)
.then((Result<void> _) {
_syncNotes(); _syncNotes();
numChanges += 1; numChanges += 1;
notifyListeners(); notifyListeners();
@ -421,15 +435,19 @@ class GitJournalRepo with ChangeNotifier {
}); });
} }
void removeNote(Note note) async { void removeNote(Note note) => removeNotes([note]);
void removeNotes(List<Note> notes) async {
logEvent(Event.NoteDeleted); logEvent(Event.NoteDeleted);
return _opLock.synchronized(() async { return _opLock.synchronized(() async {
Log.d("Got removeNote lock"); Log.d("Got removeNote lock");
// FIXME: What if the Note hasn't yet been saved? // FIXME: What if the Note hasn't yet been saved?
note.parent.remove(note); for (var note in notes) {
_gitRepo.removeNote(note).then((Result<void> _) async { note.parent.remove(note);
}
_gitRepo.removeNotes(notes).then((Result<void> _) async {
numChanges += 1; numChanges += 1;
notifyListeners(); notifyListeners();
// FIXME: Is there a way of figuring this amount dynamically? // FIXME: Is there a way of figuring this amount dynamically?

View File

@ -335,7 +335,7 @@ class NoteEditorState extends State<NoteEditor>
if (settings.confirmDelete) { if (settings.confirmDelete) {
shouldDelete = await showDialog( shouldDelete = await showDialog(
context: context, context: context,
builder: (context) => NoteDeleteDialog(), builder: (context) => const NoteDeleteDialog(num: 1),
); );
} }
if (shouldDelete == true) { if (shouldDelete == true) {

View File

@ -11,18 +11,22 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:gitjournal/generated/locale_keys.g.dart'; import 'package:gitjournal/generated/locale_keys.g.dart';
class NoteDeleteDialog extends StatelessWidget { class NoteDeleteDialog extends StatelessWidget {
final int num;
const NoteDeleteDialog({Key? key, required this.num}) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AlertDialog( return AlertDialog(
title: Text(tr(LocaleKeys.widgets_NoteDeleteDialog_title)), title: Text(LocaleKeys.widgets_NoteDeleteDialog_title.plural(num)),
actions: <Widget>[ actions: <Widget>[
TextButton( TextButton(
onPressed: () => Navigator.of(context).pop(false), onPressed: () => Navigator.of(context).pop(false),
child: Text(tr(LocaleKeys.widgets_NoteDeleteDialog_no)), child: Text(LocaleKeys.widgets_NoteDeleteDialog_no.tr()),
), ),
TextButton( TextButton(
onPressed: () => Navigator.of(context).pop(true), onPressed: () => Navigator.of(context).pop(true),
child: Text(tr(LocaleKeys.widgets_NoteDeleteDialog_yes)), child: Text(LocaleKeys.widgets_NoteDeleteDialog_yes.tr()),
), ),
], ],
); );