11import 'package:flutter/material.dart' ;
2+ import 'package:flutter/services.dart' ;
23import 'package:go_router/go_router.dart' ;
34import 'package:papyrus/data/data_store.dart' ;
45import 'package:papyrus/models/active_filter.dart' ;
56import 'package:papyrus/models/book.dart' ;
67import 'package:papyrus/providers/library_provider.dart' ;
78import 'package:papyrus/themes/design_tokens.dart' ;
9+ import 'package:papyrus/utils/bulk_book_actions.dart' ;
810import 'package:papyrus/utils/search_query_parser.dart' ;
911import 'package:papyrus/widgets/filter/filter_bottom_sheet.dart' ;
1012import 'package:papyrus/widgets/filter/filter_dialog.dart' ;
1113import 'package:papyrus/widgets/library/book_grid.dart' ;
1214import 'package:papyrus/widgets/library/book_list_item.dart' ;
1315import 'package:papyrus/widgets/library/library_drawer.dart' ;
1416import 'package:papyrus/widgets/library/library_filter_chips.dart' ;
17+ import 'package:papyrus/widgets/library/selection_header.dart' ;
1518import 'package:papyrus/widgets/search/library_search_bar.dart' ;
1619import 'package:papyrus/widgets/add_book/add_book_choice_sheet.dart' ;
1720import 'package:papyrus/widgets/shared/empty_state.dart' ;
@@ -113,61 +116,74 @@ class _LibraryPageState extends State<LibraryPage> {
113116 List <Book > books,
114117 LibraryProvider libraryProvider,
115118 ) {
119+ final isSelectionMode = libraryProvider.isSelectionMode;
120+
116121 return Scaffold (
117122 key: _scaffoldKey,
118123 drawer: const LibraryDrawer (),
119124 body: SafeArea (
120125 child: Column (
121126 children: [
122- // Search bar section with drawer button
127+ // Header: selection header or normal header
123128 Padding (
124129 padding: const EdgeInsets .only (
125130 top: Spacing .md,
126131 left: Spacing .md,
127132 right: Spacing .md,
128133 ),
129- child: Row (
130- children: [
131- // Drawer hamburger button
132- IconButton (
133- icon: const Icon (Icons .menu),
134- onPressed: () {
135- _scaffoldKey.currentState? .openDrawer ();
136- },
137- tooltip: 'Library sections' ,
138- ),
139- const SizedBox (width: Spacing .xs),
140- // Search bar
141- Expanded (child: _buildSearchBar (libraryProvider)),
142- const SizedBox (width: Spacing .sm),
143- _buildSortButton (libraryProvider),
144- ],
145- ),
134+ child: isSelectionMode
135+ ? SelectionHeader (
136+ selectedCount: libraryProvider.selectedCount,
137+ totalCount: books.length,
138+ onClose: libraryProvider.exitSelectionMode,
139+ onSelectAll: () => libraryProvider.selectAll (
140+ books.map ((b) => b.id).toList (),
141+ ),
142+ onDeselectAll: libraryProvider.deselectAll,
143+ )
144+ : Row (
145+ children: [
146+ // Drawer hamburger button
147+ IconButton (
148+ icon: const Icon (Icons .menu),
149+ onPressed: () {
150+ _scaffoldKey.currentState? .openDrawer ();
151+ },
152+ tooltip: 'Library sections' ,
153+ ),
154+ const SizedBox (width: Spacing .xs),
155+ // Search bar
156+ Expanded (child: _buildSearchBar (libraryProvider)),
157+ const SizedBox (width: Spacing .sm),
158+ _buildSortButton (libraryProvider),
159+ ],
160+ ),
146161 ),
147162
148163 // Quick filter chips
149164 const LibraryFilterChips (),
150165
151166 // View toggle row
152- Padding (
153- padding: const EdgeInsets .only (
154- left: Spacing .md,
155- right: Spacing .md,
156- bottom: Spacing .md,
157- ),
158- child: Row (
159- mainAxisAlignment: MainAxisAlignment .spaceBetween,
160- children: [
161- Text (
162- '${books .length } ${books .length == 1 ? 'book' : 'books' }' ,
163- style: Theme .of (context).textTheme.bodyMedium? .copyWith (
164- color: Theme .of (context).colorScheme.onSurfaceVariant,
167+ if (! isSelectionMode)
168+ Padding (
169+ padding: const EdgeInsets .only (
170+ left: Spacing .md,
171+ right: Spacing .md,
172+ bottom: Spacing .md,
173+ ),
174+ child: Row (
175+ mainAxisAlignment: MainAxisAlignment .spaceBetween,
176+ children: [
177+ Text (
178+ '${books .length } ${books .length == 1 ? 'book' : 'books' }' ,
179+ style: Theme .of (context).textTheme.bodyMedium? .copyWith (
180+ color: Theme .of (context).colorScheme.onSurfaceVariant,
181+ ),
165182 ),
166- ),
167- _buildViewToggle (libraryProvider) ,
168- ] ,
183+ _buildViewToggle (libraryProvider ),
184+ ] ,
185+ ) ,
169186 ),
170- ),
171187
172188 // Book grid or list
173189 Expanded (
@@ -184,10 +200,15 @@ class _LibraryPageState extends State<LibraryPage> {
184200 ],
185201 ),
186202 ),
187- floatingActionButton: FloatingActionButton (
188- onPressed: () => AddBookChoiceSheet .show (context),
189- child: const Icon (Icons .add),
190- ),
203+ floatingActionButton: isSelectionMode
204+ ? null
205+ : FloatingActionButton (
206+ onPressed: () => AddBookChoiceSheet .show (context),
207+ child: const Icon (Icons .add),
208+ ),
209+ bottomNavigationBar: isSelectionMode
210+ ? buildMobileBottomActionBar (context, libraryProvider)
211+ : null ,
191212 );
192213 }
193214
@@ -484,80 +505,108 @@ class _LibraryPageState extends State<LibraryPage> {
484505 LibraryProvider libraryProvider,
485506 ) {
486507 const double controlHeight = 40.0 ;
508+ final isSelectionMode = libraryProvider.isSelectionMode;
487509
488- return Scaffold (
489- body: Column (
490- crossAxisAlignment: CrossAxisAlignment .start,
491- children: [
492- // Header row
493- Container (
494- padding: const EdgeInsets .only (
495- top: Spacing .lg,
496- left: Spacing .lg,
497- right: Spacing .lg,
498- ),
499- child: LayoutBuilder (
500- builder: (context, constraints) {
501- final useCompactLayout = constraints.maxWidth < 800 ;
502-
503- if (useCompactLayout) {
504- return Column (
505- crossAxisAlignment: CrossAxisAlignment .stretch,
506- children: [
507- Row (
508- children: [
509- Expanded (child: _buildSearchBar (libraryProvider)),
510- const SizedBox (width: Spacing .sm),
511- _buildSortButton (libraryProvider),
512- ],
510+ return CallbackShortcuts (
511+ bindings: {
512+ const SingleActivator (LogicalKeyboardKey .escape): () {
513+ if (libraryProvider.isSelectionMode) {
514+ libraryProvider.exitSelectionMode ();
515+ }
516+ },
517+ },
518+ child: Focus (
519+ autofocus: true ,
520+ child: Scaffold (
521+ body: Column (
522+ crossAxisAlignment: CrossAxisAlignment .start,
523+ children: [
524+ // Header row
525+ Container (
526+ padding: const EdgeInsets .only (
527+ top: Spacing .lg,
528+ left: Spacing .lg,
529+ right: Spacing .lg,
530+ ),
531+ child: isSelectionMode
532+ ? SelectionHeader (
533+ selectedCount: libraryProvider.selectedCount,
534+ totalCount: books.length,
535+ onClose: libraryProvider.exitSelectionMode,
536+ onSelectAll: () => libraryProvider.selectAll (
537+ books.map ((b) => b.id).toList (),
538+ ),
539+ onDeselectAll: libraryProvider.deselectAll,
540+ actions: buildBulkActionBar (context, libraryProvider),
541+ )
542+ : LayoutBuilder (
543+ builder: (context, constraints) {
544+ final useCompactLayout = constraints.maxWidth < 800 ;
545+
546+ if (useCompactLayout) {
547+ return Column (
548+ crossAxisAlignment: CrossAxisAlignment .stretch,
549+ children: [
550+ Row (
551+ children: [
552+ Expanded (
553+ child: _buildSearchBar (libraryProvider),
554+ ),
555+ const SizedBox (width: Spacing .sm),
556+ _buildSortButton (libraryProvider),
557+ ],
558+ ),
559+ const SizedBox (height: Spacing .md),
560+ Row (
561+ children: [
562+ const Spacer (),
563+ _buildViewToggle (libraryProvider),
564+ const SizedBox (width: Spacing .sm),
565+ _buildAddBookButton (controlHeight),
566+ ],
567+ ),
568+ ],
569+ );
570+ }
571+
572+ return Row (
573+ children: [
574+ Expanded (child: _buildSearchBar (libraryProvider)),
575+ const SizedBox (width: Spacing .md),
576+ _buildSortButton (libraryProvider),
577+ const SizedBox (width: Spacing .md),
578+ _buildViewToggle (libraryProvider),
579+ const SizedBox (width: Spacing .md),
580+ _buildAddBookButton (controlHeight),
581+ ],
582+ );
583+ },
513584 ),
514- const SizedBox (height: Spacing .md),
515- Row (
516- children: [
517- const Spacer (),
518- _buildViewToggle (libraryProvider),
519- const SizedBox (width: Spacing .sm),
520- _buildAddBookButton (controlHeight),
521- ],
585+ ),
586+ // Filter chips
587+ const LibraryFilterChips (horizontalPadding: Spacing .lg),
588+ // Book grid or list
589+ Expanded (
590+ child: books.isEmpty
591+ ? _buildEmptyState ()
592+ : libraryProvider.isListView
593+ ? _buildBookList (context, books)
594+ : BookGrid (
595+ books: books,
596+ onBookTap: (book) =>
597+ _navigateToBookDetails (context, book),
522598 ),
523- ],
524- );
525- }
526-
527- return Row (
528- children: [
529- Expanded (child: _buildSearchBar (libraryProvider)),
530- const SizedBox (width: Spacing .md),
531- _buildSortButton (libraryProvider),
532- const SizedBox (width: Spacing .md),
533- _buildViewToggle (libraryProvider),
534- const SizedBox (width: Spacing .md),
535- _buildAddBookButton (controlHeight),
536- ],
537- );
538- },
539- ),
540- ),
541- // Filter chips
542- const LibraryFilterChips (horizontalPadding: Spacing .lg),
543- // Book grid or list
544- Expanded (
545- child: books.isEmpty
546- ? _buildEmptyState ()
547- : libraryProvider.isListView
548- ? _buildBookList (context, books)
549- : BookGrid (
550- books: books,
551- onBookTap: (book) => _navigateToBookDetails (context, book),
552- ),
599+ ),
600+ ],
553601 ),
554- ] ,
602+ ) ,
555603 ),
556604 );
557605 }
558606
559607 Widget _buildBookList (BuildContext context, List <Book > books) {
560- final libraryProvider = context.read <LibraryProvider >();
608+ final libraryProvider = context.watch <LibraryProvider >();
609+ final isSelectionMode = libraryProvider.isSelectionMode;
561610
562611 return ListView .builder (
563612 padding: const EdgeInsets .symmetric (horizontal: Spacing .md),
@@ -572,6 +621,9 @@ class _LibraryPageState extends State<LibraryPage> {
572621 book: book,
573622 isFavorite: isFavorite,
574623 onTap: () => _navigateToBookDetails (context, book),
624+ isSelectionMode: isSelectionMode,
625+ isSelected: libraryProvider.isBookSelected (book.id),
626+ onSelectToggle: () => libraryProvider.toggleBookSelection (book.id),
575627 );
576628 },
577629 );
0 commit comments