Skip to content

Commit f530dcd

Browse files
committed
feat(poems): add multi-select functionality and enhance poem card interactions
1 parent 2230db6 commit f530dcd

File tree

5 files changed

+476
-268
lines changed

5 files changed

+476
-268
lines changed

lib/database/database.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,4 +112,8 @@ class Database extends _$Database {
112112
Future<int> deletePoem(PoemModel model) {
113113
return delete(poem).delete(model);
114114
}
115+
116+
Future<int> deletePoems(Iterable<int> poemIds) {
117+
return poem.deleteWhere((f) => f.id.isIn(poemIds));
118+
}
115119
}

lib/screens/poems_screen/poems_screen.dart

Lines changed: 219 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import 'package:flutter_local_notifications/flutter_local_notifications.dart';
66
import 'package:flutter_markdown/flutter_markdown.dart';
77
import 'package:flutter_riverpod/flutter_riverpod.dart';
88
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
9+
import 'package:heartry/screens/poems_screen/providers/multi_select_provider.dart';
910
import '../../database/database.dart';
1011
import '../../init_get_it.dart';
1112
import 'package:url_launcher/url_launcher_string.dart';
@@ -15,7 +16,10 @@ import '../../providers/app_version_manager_provider.dart';
1516
import '../../providers/changelog_provider.dart';
1617
import '../../providers/list_grid_provider.dart';
1718
import '../../providers/stream_poem_provider.dart';
19+
import '../../utils/share_helper.dart';
20+
import '../../widgets/share_option_list.dart';
1821
import '../profile_screen/profile_screen.dart';
22+
import '../reader_screen/reader_screen.dart';
1923
import '../settings_screen/settings_screen.dart';
2024
import '../writing_screen/writing_screen.dart';
2125
import 'widgets/poem_card.dart';
@@ -176,6 +180,24 @@ class _ChangelogDialog extends StatelessWidget {
176180
class _CAppBar extends ConsumerWidget {
177181
const _CAppBar();
178182

183+
@override
184+
Widget build(BuildContext context, WidgetRef ref) {
185+
final showMultiOption = ref.watch(multiSelectEnabledProvider);
186+
187+
return AnimatedCrossFade(
188+
firstChild: _DefaultAppBar(),
189+
secondChild: _Toolbar(),
190+
crossFadeState: showMultiOption
191+
? CrossFadeState.showSecond
192+
: CrossFadeState.showFirst,
193+
duration: const Duration(milliseconds: 100),
194+
);
195+
}
196+
}
197+
198+
class _DefaultAppBar extends ConsumerWidget {
199+
const _DefaultAppBar();
200+
179201
@override
180202
Widget build(BuildContext context, WidgetRef ref) {
181203
final imagePath =
@@ -209,39 +231,7 @@ class _CAppBar extends ConsumerWidget {
209231
? const Icon(Icons.list_alt_rounded)
210232
: const Icon(Icons.grid_view),
211233
),
212-
const SizedBox(width: 10),
213-
SearchAnchor(
214-
isFullScreen: true,
215-
builder: (context, controller) {
216-
return IconButton(
217-
onPressed: () {
218-
controller.openView();
219-
},
220-
icon: Icon(Icons.search),
221-
);
222-
},
223-
suggestionsBuilder: (context, controller) async {
224-
final query = controller.text;
225-
if (query.isEmpty) return [];
226-
227-
final poems = await locator<Database>().searchPoems(query);
228-
return poems.map((poem) => PoemCard(model: poem));
229-
},
230-
viewBuilder: (suggestions) => _PoemsLayout(
231-
isGrid: isGrid,
232-
itemBuilder: (context, index) {
233-
final poems = suggestions.toList();
234-
235-
return Padding(
236-
padding: !isGrid
237-
? const EdgeInsets.symmetric(horizontal: 10, vertical: 5)
238-
: EdgeInsets.zero,
239-
child: poems[index],
240-
);
241-
},
242-
itemCount: suggestions.length,
243-
),
244-
),
234+
_SearchIcon(isGrid: isGrid),
245235
const SizedBox(width: 10),
246236
GestureDetector(
247237
onTap: () {
@@ -263,13 +253,190 @@ class _CAppBar extends ConsumerWidget {
263253
}
264254
}
265255

256+
class _Toolbar extends ConsumerWidget {
257+
const _Toolbar();
258+
259+
@override
260+
Widget build(BuildContext context, WidgetRef ref) {
261+
final selectedPoems = ref.watch(selectedPoemsProvider);
262+
263+
return Padding(
264+
padding: const EdgeInsets.only(top: 15.0, left: 5.0, right: 5.0),
265+
child: Row(
266+
children: [
267+
IconButton(
268+
icon: const Icon(Icons.close_rounded),
269+
onPressed: () => ref.read(selectedPoemsProvider.notifier).clear(),
270+
),
271+
Text(
272+
selectedPoems.length.toString(),
273+
style: Theme.of(context).textTheme.bodyLarge,
274+
),
275+
const Spacer(),
276+
if (selectedPoems.length == 1) ...[
277+
IconButton(
278+
icon: const Icon(Icons.share),
279+
onPressed: () => _shareClicked(context, selectedPoems.first),
280+
),
281+
IconButton(
282+
icon: const Icon(Icons.remove_red_eye_rounded),
283+
onPressed: () =>
284+
_navigateToReaderScreen(context, selectedPoems.first),
285+
),
286+
],
287+
IconButton(
288+
icon: const Icon(Icons.delete),
289+
onPressed: () => _showWarning(context, selectedPoems, ref),
290+
),
291+
],
292+
),
293+
);
294+
}
295+
296+
void _shareClicked(BuildContext context, PoemModel poem) {
297+
showModalBottomSheet<void>(
298+
context: context,
299+
builder: (context) => Consumer(
300+
builder: (context, ref, child) {
301+
final poet =
302+
ref //
303+
.watch(configProvider)
304+
.whenOrNull(data: (data) => data.name);
305+
306+
return ShareOptionList(
307+
onShareAsImage: () => ShareHelper.shareAsImage(
308+
context,
309+
title: poem.title,
310+
poem: poem.poem,
311+
poet: poet ?? "Unknown",
312+
),
313+
onShareAsText: () => ShareHelper.shareAsText(
314+
title: poem.title,
315+
poem: poem.poem,
316+
poet: poet ?? "Unknown",
317+
),
318+
);
319+
},
320+
),
321+
);
322+
}
323+
324+
void _navigateToReaderScreen(BuildContext context, PoemModel poem) {
325+
Navigator.push<void>(
326+
context,
327+
MaterialPageRoute(builder: (_) => ReaderScreen(model: poem)),
328+
);
329+
}
330+
331+
void _showWarning(
332+
BuildContext context,
333+
List<PoemModel> selectedPoems,
334+
WidgetRef ref,
335+
) {
336+
showDialog<void>(
337+
context: context,
338+
builder: (_) => AlertDialog(
339+
title: const Text("Do you really want to delete it?"),
340+
content: const Text(
341+
"There would be no other way to get back you art."
342+
" Are you really sure?",
343+
),
344+
actions: [
345+
TextButton(
346+
onPressed: () => _delete(context, selectedPoems, ref),
347+
child: const Text("Yes"),
348+
),
349+
FilledButton(
350+
onPressed: () => Navigator.pop(context),
351+
child: const Text("No"),
352+
),
353+
],
354+
),
355+
);
356+
}
357+
358+
Future<void> _delete(
359+
BuildContext context,
360+
List<PoemModel> selectedPoems,
361+
WidgetRef ref,
362+
) async {
363+
final navigator = Navigator.of(context);
364+
final scaffoldMessenger = ScaffoldMessenger.of(context);
365+
366+
final result = await locator<Database>().deletePoems(
367+
selectedPoems.map((p) => p.id!),
368+
);
369+
370+
final String msg = result == 0 ? "Failed to delete" : "Deleted";
371+
372+
navigator.pop();
373+
scaffoldMessenger.showSnackBar(SnackBar(content: Text(msg)));
374+
ref.read(selectedPoemsProvider.notifier).clear();
375+
}
376+
}
377+
378+
class _SearchIcon extends StatelessWidget {
379+
const _SearchIcon({required this.isGrid});
380+
381+
final bool isGrid;
382+
383+
@override
384+
Widget build(BuildContext context) {
385+
return SearchAnchor(
386+
isFullScreen: true,
387+
builder: (context, controller) {
388+
return IconButton(
389+
onPressed: () {
390+
controller.openView();
391+
},
392+
icon: Icon(Icons.search),
393+
);
394+
},
395+
suggestionsBuilder: (context, controller) async {
396+
final query = controller.text;
397+
if (query.isEmpty) return [];
398+
399+
final poems = await locator<Database>().searchPoems(query);
400+
401+
return poems.map(
402+
(poem) => PoemCard(
403+
model: poem,
404+
onPressed: () {
405+
Navigator.push<void>(
406+
context,
407+
MaterialPageRoute(builder: (_) => WritingScreen(model: poem)),
408+
);
409+
},
410+
),
411+
);
412+
},
413+
viewBuilder: (suggestions) => _PoemsLayout(
414+
isGrid: isGrid,
415+
itemBuilder: (context, index) {
416+
final poems = suggestions.toList();
417+
418+
return Padding(
419+
padding: !isGrid
420+
? const EdgeInsets.symmetric(horizontal: 10, vertical: 5)
421+
: EdgeInsets.zero,
422+
child: poems[index],
423+
);
424+
},
425+
itemCount: suggestions.length,
426+
),
427+
);
428+
}
429+
}
430+
266431
class _CBody extends ConsumerWidget {
267432
const _CBody();
268433

269434
@override
270435
Widget build(BuildContext context, WidgetRef ref) {
271436
final isGrid = ref.watch(listGridProvider);
272437
final poems = ref.watch(streamPoemProvider);
438+
final selectedPoems = ref.watch(selectedPoemsProvider);
439+
final multiSelectedEnabled = ref.watch(multiSelectEnabledProvider);
273440

274441
return poems.when(
275442
data: (poems) {
@@ -298,13 +465,32 @@ class _CBody extends ConsumerWidget {
298465
itemCount: poems.length,
299466
itemBuilder: (context, index) {
300467
final poem = poems[index];
468+
final isSelected =
469+
selectedPoems.indexWhere((t) => t.id == poem.id) != -1;
470+
301471
return Padding(
302472
padding: !isGrid
303473
? const EdgeInsets.symmetric(horizontal: 10, vertical: 5)
304474
: EdgeInsets.zero,
305475
child: PoemCard(
306476
model: poem,
477+
isSelected: isSelected,
307478
key: ValueKey("${poem.lastEdit}-${poem.id}"),
479+
onPressed: () {
480+
if (multiSelectedEnabled) {
481+
ref.read(selectedPoemsProvider.notifier).toggle(poem);
482+
return;
483+
}
484+
Navigator.push<void>(
485+
context,
486+
MaterialPageRoute(
487+
builder: (_) => WritingScreen(model: poem),
488+
),
489+
);
490+
},
491+
onLongPress: () {
492+
ref.read(selectedPoemsProvider.notifier).toggle(poem);
493+
},
308494
),
309495
);
310496
},
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import 'package:flutter_riverpod/flutter_riverpod.dart';
2+
import 'package:heartry/database/database.dart';
3+
4+
final multiSelectEnabledProvider = Provider(
5+
(ref) => ref.watch(selectedPoemsProvider).isNotEmpty,
6+
);
7+
8+
final selectedPoemsProvider = StateNotifierProvider((_) => SelectedPoems());
9+
10+
class SelectedPoems extends StateNotifier<List<PoemModel>> {
11+
SelectedPoems() : super([]);
12+
13+
void toggle(PoemModel poem) {
14+
final index = state.indexWhere((t) => t.id == poem.id);
15+
16+
if (index == -1) {
17+
state = [...state, poem];
18+
} else {
19+
state = [...state..removeAt(index)];
20+
}
21+
}
22+
23+
void add(PoemModel poem) {
24+
state = [...state, poem];
25+
}
26+
27+
void remove(PoemModel poem) {
28+
final updated = state..removeWhere((t) => t.id == poem.id);
29+
state = [...updated];
30+
}
31+
32+
void clear() {
33+
state = [];
34+
}
35+
}

0 commit comments

Comments
 (0)