diff --git a/assets/langs/en.yaml b/assets/langs/en.yaml index ecd4b478..a699c870 100644 --- a/assets/langs/en.yaml +++ b/assets/langs/en.yaml @@ -370,7 +370,12 @@ widgets: actions: moveToFolder: Move To Folder 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 no: No NoteEditor: diff --git a/lib/core/commit_message_builder.dart b/lib/core/commit_message_builder.dart index 00dd8ba8..ecfc6e8d 100644 --- a/lib/core/commit_message_builder.dart +++ b/lib/core/commit_message_builder.dart @@ -18,8 +18,28 @@ class CommitMessageBuilder { String moveNote(String oldSpec, String newSpec) => "Moved Note $oldSpec -> $newSpec"; + String moveNotes(List oldSpecs, List 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 removeFolder(String spec) => "Removed Folder $spec"; + String removeNotes(Iterable 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"; } diff --git a/lib/core/git_repo.dart b/lib/core/git_repo.dart index ad4c5909..5b661434 100644 --- a/lib/core/git_repo.dart +++ b/lib/core/git_repo.dart @@ -163,25 +163,32 @@ class GitNoteRepository { return _addAllAndCommit(msg); } - Future> moveNote( - String oldFullPath, - String newFullPath, + Future> moveNotes( + List oldPaths, + List newPaths, + String newFolderPath, ) async { var repoPath = gitRepoPath.endsWith('/') ? gitRepoPath : '$gitRepoPath/'; - var oldSpec = oldFullPath.substring(repoPath.length); - var newSpec = newFullPath.substring(repoPath.length); - var msg = messageBuilder.moveNote(oldSpec, newSpec); + var oldSpecs = oldPaths.map((p) => p.substring(repoPath.length)).toList(); + var newSpecs = newPaths.map((p) => p.substring(repoPath.length)).toList(); + var msg = oldPaths.length == 1 + ? messageBuilder.moveNote(oldSpecs.first, newSpecs.first) + : messageBuilder.moveNotes(oldSpecs, newSpecs); return _addAllAndCommit(msg); } - Future> removeNote(Note note) async { + Future> removeNotes(List notes) async { return catchAll(() async { // We are not calling note.remove() as gitRm will also remove the file - var spec = note.pathSpec(); - await _rm(spec).throwOnError(); + for (var note in notes) { + var spec = note.pathSpec(); + await _rm(spec).throwOnError(); + } 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, authorName: config.gitAuthor, ).throwOnError(); diff --git a/lib/folder_views/common.dart b/lib/folder_views/common.dart index edaa1ece..0bb7cf52 100644 --- a/lib/folder_views/common.dart +++ b/lib/folder_views/common.dart @@ -34,7 +34,7 @@ Widget buildFolderView({ required NoteSelectedFunction noteTapped, required NoteSelectedFunction noteLongPressed, required NoteBoolPropertyFunction isNoteSelected, - required String searchTerm, + String searchTerm = "", }) { switch (viewType) { case FolderViewType.Standard: diff --git a/lib/folder_views/folder_view.dart b/lib/folder_views/folder_view.dart index ee7d84d7..5864b8c0 100644 --- a/lib/folder_views/folder_view.dart +++ b/lib/folder_views/folder_view.dart @@ -68,8 +68,8 @@ class _FolderViewState extends State { var _headerType = StandardViewHeader.TitleGenerated; bool _showSummary = true; - Note? selectedNote; - bool get inSelectionMode => selectedNote != null; + var selectedNotes = []; + bool get inSelectionMode => selectedNotes.isNotEmpty; @override void initState() { @@ -119,7 +119,7 @@ class _FolderViewState extends State { Widget _buildBody(BuildContext context) { var title = widget.notesFolder.publicName; if (inSelectionMode) { - title = NumberFormat.compact().format(1); + title = NumberFormat.compact().format(selectedNotes.length); } var folderView = buildFolderView( @@ -128,20 +128,9 @@ class _FolderViewState extends State { emptyText: tr(LocaleKeys.screens_folder_view_empty), header: _headerType, showSummary: _showSummary, - noteTapped: (Note note) { - if (!inSelectionMode) { - openNoteEditor(context, note, widget.notesFolder); - } else { - _resetSelection(); - } - }, - noteLongPressed: (Note note) { - setState(() { - selectedNote = note; - }); - }, - isNoteSelected: (n) => n == selectedNote, - searchTerm: "", + noteTapped: _noteTapped, + noteLongPressed: _noteLongPress, + isNoteSelected: (n) => selectedNotes.contains(n), ); Widget pinnedFolderView = const SizedBox(); @@ -152,23 +141,11 @@ class _FolderViewState extends State { emptyText: null, header: _headerType, showSummary: _showSummary, - noteTapped: (Note note) { - if (!inSelectionMode) { - openNoteEditor(context, note, widget.notesFolder); - } else { - _resetSelection(); - } - }, - noteLongPressed: (Note note) { - setState(() { - selectedNote = note; - }); - }, - isNoteSelected: (n) => n == selectedNote, - searchTerm: "", + noteTapped: _noteTapped, + noteLongPressed: _noteLongPress, + isNoteSelected: (n) => selectedNotes.contains(n), ); } - // assert(folderView is SliverWithKeepAliveWidget); var settings = Provider.of(context); final showButtomMenuBar = settings.bottomMenuBar; @@ -231,6 +208,37 @@ class _FolderViewState extends State { ); } + 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 Widget build(BuildContext context) { var createButton = FloatingActionButton( @@ -558,7 +566,7 @@ class _FolderViewState extends State { onSelected: (NoteSelectedExtraActions choice) { switch (choice) { case NoteSelectedExtraActions.MoveToFolder: - _moveNoteToFolder(); + _moveSelectedNotesToFolder(); break; } }, @@ -572,50 +580,48 @@ class _FolderViewState extends State { ); return [ - IconButton( - icon: const Icon(Icons.share), - onPressed: () async { - await shareNote(selectedNote!); - _resetSelection(); - }, - ), + if (selectedNotes.length == 1) + IconButton( + icon: const Icon(Icons.share), + onPressed: () async { + await shareNote(selectedNotes.first); + _resetSelection(); + }, + ), IconButton( icon: const Icon(Icons.delete), - onPressed: _deleteNote, + onPressed: _deleteSelectedNotes, ), extraActions, ]; } - void _deleteNote() async { - var note = selectedNote; - + void _deleteSelectedNotes() async { var settings = Provider.of(context, listen: false); var shouldDelete = true; if (settings.confirmDelete) { - shouldDelete = await showDialog( - context: context, - builder: (context) => NoteDeleteDialog(), - ); + shouldDelete = (await showDialog( + context: context, + builder: (context) => NoteDeleteDialog(num: selectedNotes.length), + )) == + true; } if (shouldDelete == true) { - var stateContainer = context.read(); - stateContainer.removeNote(note!); + var repo = context.read(); + repo.removeNotes(selectedNotes); } _resetSelection(); } - void _moveNoteToFolder() async { - var note = selectedNote!; - + void _moveSelectedNotesToFolder() async { var destFolder = await showDialog( context: context, builder: (context) => FolderSelectionDialog(), ); if (destFolder != null) { var repo = context.read(); - repo.moveNote(note, destFolder); + repo.moveNotes(selectedNotes, destFolder); } _resetSelection(); @@ -623,7 +629,7 @@ class _FolderViewState extends State { void _resetSelection() { setState(() { - selectedNote = null; + selectedNotes = []; }); } } diff --git a/lib/repository.dart b/lib/repository.dart index bae5616c..6f2b82b2 100644 --- a/lib/repository.dart +++ b/lib/repository.dart @@ -366,8 +366,15 @@ class GitJournalRepo with ChangeNotifier { }); } - void moveNote(Note note, NotesFolderFS destFolder) async { - if (destFolder.folderPath == note.parent.folderPath) { + Future moveNote(Note note, NotesFolderFS destFolder) => + moveNotes([note], destFolder); + + Future moveNotes(List notes, NotesFolderFS destFolder) async { + notes = notes + .where((n) => n.parent.folderPath != destFolder.folderPath) + .toList(); + + if (notes.isEmpty) { return; } @@ -375,10 +382,17 @@ class GitJournalRepo with ChangeNotifier { return _opLock.synchronized(() async { Log.d("Got moveNote lock"); - var oldNotePath = note.filePath; - NotesFolderFS.moveNote(note, destFolder); + var oldPaths = []; + var newPaths = []; + for (var note in notes) { + oldPaths.add(note.filePath); + NotesFolderFS.moveNote(note, destFolder); + newPaths.add(note.filePath); + } - _gitRepo.moveNote(oldNotePath, note.filePath).then((Result _) { + _gitRepo + .moveNotes(oldPaths, newPaths, destFolder.folderPath) + .then((Result _) { _syncNotes(); numChanges += 1; notifyListeners(); @@ -421,15 +435,19 @@ class GitJournalRepo with ChangeNotifier { }); } - void removeNote(Note note) async { + void removeNote(Note note) => removeNotes([note]); + + void removeNotes(List notes) async { logEvent(Event.NoteDeleted); return _opLock.synchronized(() async { Log.d("Got removeNote lock"); // FIXME: What if the Note hasn't yet been saved? - note.parent.remove(note); - _gitRepo.removeNote(note).then((Result _) async { + for (var note in notes) { + note.parent.remove(note); + } + _gitRepo.removeNotes(notes).then((Result _) async { numChanges += 1; notifyListeners(); // FIXME: Is there a way of figuring this amount dynamically? diff --git a/lib/screens/note_editor.dart b/lib/screens/note_editor.dart index 4f45e953..2ec9b064 100644 --- a/lib/screens/note_editor.dart +++ b/lib/screens/note_editor.dart @@ -335,7 +335,7 @@ class NoteEditorState extends State if (settings.confirmDelete) { shouldDelete = await showDialog( context: context, - builder: (context) => NoteDeleteDialog(), + builder: (context) => const NoteDeleteDialog(num: 1), ); } if (shouldDelete == true) { diff --git a/lib/widgets/note_delete_dialog.dart b/lib/widgets/note_delete_dialog.dart index d4d1e046..ed7cde8d 100644 --- a/lib/widgets/note_delete_dialog.dart +++ b/lib/widgets/note_delete_dialog.dart @@ -11,18 +11,22 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:gitjournal/generated/locale_keys.g.dart'; class NoteDeleteDialog extends StatelessWidget { + final int num; + + const NoteDeleteDialog({Key? key, required this.num}) : super(key: key); + @override Widget build(BuildContext context) { return AlertDialog( - title: Text(tr(LocaleKeys.widgets_NoteDeleteDialog_title)), + title: Text(LocaleKeys.widgets_NoteDeleteDialog_title.plural(num)), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(false), - child: Text(tr(LocaleKeys.widgets_NoteDeleteDialog_no)), + child: Text(LocaleKeys.widgets_NoteDeleteDialog_no.tr()), ), TextButton( onPressed: () => Navigator.of(context).pop(true), - child: Text(tr(LocaleKeys.widgets_NoteDeleteDialog_yes)), + child: Text(LocaleKeys.widgets_NoteDeleteDialog_yes.tr()), ), ], );