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:
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:

View File

@ -18,8 +18,28 @@ class CommitMessageBuilder {
String moveNote(String oldSpec, String 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 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";
}

View File

@ -163,25 +163,32 @@ class GitNoteRepository {
return _addAllAndCommit(msg);
}
Future<Result<void>> moveNote(
String oldFullPath,
String newFullPath,
Future<Result<void>> moveNotes(
List<String> oldPaths,
List<String> 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<Result<void>> removeNote(Note note) async {
Future<Result<void>> removeNotes(List<Note> 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();

View File

@ -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:

View File

@ -68,8 +68,8 @@ class _FolderViewState extends State<FolderView> {
var _headerType = StandardViewHeader.TitleGenerated;
bool _showSummary = true;
Note? selectedNote;
bool get inSelectionMode => selectedNote != null;
var selectedNotes = <Note>[];
bool get inSelectionMode => selectedNotes.isNotEmpty;
@override
void initState() {
@ -119,7 +119,7 @@ class _FolderViewState extends State<FolderView> {
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<FolderView> {
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<FolderView> {
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<Settings>(context);
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
Widget build(BuildContext context) {
var createButton = FloatingActionButton(
@ -558,7 +566,7 @@ class _FolderViewState extends State<FolderView> {
onSelected: (NoteSelectedExtraActions choice) {
switch (choice) {
case NoteSelectedExtraActions.MoveToFolder:
_moveNoteToFolder();
_moveSelectedNotesToFolder();
break;
}
},
@ -572,50 +580,48 @@ class _FolderViewState extends State<FolderView> {
);
return <Widget>[
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<Settings>(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<GitJournalRepo>();
stateContainer.removeNote(note!);
var repo = context.read<GitJournalRepo>();
repo.removeNotes(selectedNotes);
}
_resetSelection();
}
void _moveNoteToFolder() async {
var note = selectedNote!;
void _moveSelectedNotesToFolder() async {
var destFolder = await showDialog<NotesFolderFS>(
context: context,
builder: (context) => FolderSelectionDialog(),
);
if (destFolder != null) {
var repo = context.read<GitJournalRepo>();
repo.moveNote(note, destFolder);
repo.moveNotes(selectedNotes, destFolder);
}
_resetSelection();
@ -623,7 +629,7 @@ class _FolderViewState extends State<FolderView> {
void _resetSelection() {
setState(() {
selectedNote = null;
selectedNotes = [];
});
}
}

View File

@ -366,8 +366,15 @@ class GitJournalRepo with ChangeNotifier {
});
}
void moveNote(Note note, NotesFolderFS destFolder) async {
if (destFolder.folderPath == note.parent.folderPath) {
Future<void> moveNote(Note note, NotesFolderFS destFolder) =>
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;
}
@ -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 = <String>[];
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();
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<Note> 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<void> _) async {
for (var note in notes) {
note.parent.remove(note);
}
_gitRepo.removeNotes(notes).then((Result<void> _) async {
numChanges += 1;
notifyListeners();
// FIXME: Is there a way of figuring this amount dynamically?

View File

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

View File

@ -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: <Widget>[
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()),
),
],
);