Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 31 additions & 30 deletions lib/features/buddies/presentation/widgets/buddy_list_content.dart
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import 'package:submersion/features/buddies/domain/entities/buddy.dart';
import 'package:submersion/features/buddies/presentation/providers/buddy_providers.dart';
import 'package:submersion/features/buddies/presentation/widgets/dense_buddy_list_tile.dart';
import 'package:submersion/features/settings/presentation/providers/settings_providers.dart';
import 'package:submersion/shared/widgets/debounced_search_results.dart';

/// Content widget for the buddy list, used in master-detail layout.
///
Expand Down Expand Up @@ -605,34 +606,10 @@ class BuddySearchDelegate extends SearchDelegate<Buddy?> {
}

Widget _buildSearchResults(BuildContext context) {
final searchAsync = ref.watch(buddySearchProvider(query));

return searchAsync.when(
data: (buddies) {
if (buddies.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.search_off,
size: 64,
color: Theme.of(
context,
).colorScheme.onSurfaceVariant.withValues(alpha: 0.5),
),
const SizedBox(height: 16),
Text(
context.l10n.buddies_search_noResults(query),
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
],
),
);
}

return DebouncedSearchResults<Buddy>(
query: query,
watchProvider: (ref, q) => ref.watch(buddySearchProvider(q)),
dataBuilder: (context, buddies) {
return ListView.builder(
itemCount: buddies.length,
itemBuilder: (context, index) {
Expand All @@ -647,8 +624,32 @@ class BuddySearchDelegate extends SearchDelegate<Buddy?> {
},
);
},
loading: () => const Center(child: CircularProgressIndicator()),
error: (error, _) => Center(child: Text('Error: $error')),
emptyBuilder: (context, query) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.search_off,
size: 64,
color: Theme.of(
context,
).colorScheme.onSurfaceVariant.withValues(alpha: 0.5),
),
const SizedBox(height: 16),
Text(
context.l10n.buddies_search_noResults(query),
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
],
),
);
},
errorBuilder: (context, error) {
return Center(child: Text('Error: $error'));
},
);
}
}
169 changes: 107 additions & 62 deletions lib/features/buddies/presentation/widgets/buddy_picker.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import 'dart:async';

import 'package:flutter/material.dart';
import 'package:submersion/core/providers/provider.dart';
import 'package:go_router/go_router.dart';
Expand Down Expand Up @@ -225,6 +227,9 @@ class _BuddySelectionSheet extends ConsumerStatefulWidget {
class _BuddySelectionSheetState extends ConsumerState<_BuddySelectionSheet> {
final _searchController = TextEditingController();
String _searchQuery = '';
String _debouncedQuery = '';
Timer? _debounceTimer;
List<Buddy>? _lastSearchResults;
late List<BuddyWithRole> _localSelectedBuddies;

@override
Expand All @@ -235,15 +240,16 @@ class _BuddySelectionSheetState extends ConsumerState<_BuddySelectionSheet> {

@override
void dispose() {
_debounceTimer?.cancel();
_searchController.dispose();
super.dispose();
}

@override
Widget build(BuildContext context) {
final buddiesAsync = _searchQuery.isEmpty
final buddiesAsync = _debouncedQuery.isEmpty
? ref.watch(allBuddiesProvider)
: ref.watch(buddySearchProvider(_searchQuery));
: ref.watch(buddySearchProvider(_debouncedQuery));

return DraggableScrollableSheet(
initialChildSize: 0.7,
Expand Down Expand Up @@ -286,7 +292,12 @@ class _BuddySelectionSheetState extends ConsumerState<_BuddySelectionSheet> {
tooltip: context.l10n.buddies_action_clearSearch,
onPressed: () {
_searchController.clear();
setState(() => _searchQuery = '');
_debounceTimer?.cancel();
setState(() {
_searchQuery = '';
_debouncedQuery = '';
_lastSearchResults = null;
});
},
)
: null,
Expand All @@ -296,6 +307,22 @@ class _BuddySelectionSheetState extends ConsumerState<_BuddySelectionSheet> {
),
onChanged: (value) {
setState(() => _searchQuery = value);
_debounceTimer?.cancel();
if (value.isEmpty) {
setState(() {
_debouncedQuery = '';
_lastSearchResults = null;
});
} else {
_debounceTimer = Timer(
const Duration(milliseconds: 300),
() {
if (mounted) {
setState(() => _debouncedQuery = value);
}
},
);
}
},
Comment thread
ericgriffin marked this conversation as resolved.
),
),
Expand Down Expand Up @@ -327,6 +354,9 @@ class _BuddySelectionSheetState extends ConsumerState<_BuddySelectionSheet> {
Expanded(
child: buddiesAsync.when(
data: (buddies) {
if (_debouncedQuery.isNotEmpty) {
_lastSearchResults = buddies;
}
if (buddies.isEmpty) {
return Center(
child: Column(
Expand All @@ -353,67 +383,25 @@ class _BuddySelectionSheetState extends ConsumerState<_BuddySelectionSheet> {
);
}

return ListView.builder(
controller: scrollController,
itemCount: buddies.length,
itemBuilder: (context, index) {
final buddy = buddies[index];
final isSelected = _localSelectedBuddies.any(
(b) => b.buddy.id == buddy.id,
);
final selectedRole = _localSelectedBuddies
.where((b) => b.buddy.id == buddy.id)
.map((b) => b.role)
.firstOrNull;

return ListTile(
leading: CircleAvatar(
backgroundColor: isSelected
? Theme.of(context).colorScheme.primaryContainer
: Theme.of(
context,
).colorScheme.surfaceContainerHighest,
child: isSelected
? Icon(
Icons.check,
color: Theme.of(
context,
).colorScheme.onPrimaryContainer,
)
: Text(
buddy.initials,
style: TextStyle(
fontWeight: FontWeight.bold,
color: Theme.of(
context,
).colorScheme.onSurfaceVariant,
),
),
return _buildBuddyListView(scrollController, buddies);
},
loading: () {
if (_lastSearchResults != null &&
_lastSearchResults!.isNotEmpty) {
return Column(
children: [
const LinearProgressIndicator(),
Expanded(
child: _buildBuddyListView(
scrollController,
_lastSearchResults!,
),
),
title: Text(buddy.name),
subtitle: buddy.certificationLevel != null
? Text(buddy.certificationLevel!.displayName)
: null,
trailing: isSelected
? Chip(
label: Text(
selectedRole?.displayName ?? 'Buddy',
),
visualDensity: VisualDensity.compact,
)
: null,
onTap: () {
if (isSelected) {
_removeBuddy(buddy.id);
} else {
_showRoleSelectorForBuddy(context, buddy);
}
},
);
},
);
],
);
}
return const Center(child: CircularProgressIndicator());
},
loading: () => const Center(child: CircularProgressIndicator()),
error: (error, _) => Center(child: Text('Error: $error')),
),
),
Expand All @@ -423,6 +411,63 @@ class _BuddySelectionSheetState extends ConsumerState<_BuddySelectionSheet> {
);
}

Widget _buildBuddyListView(
ScrollController scrollController,
List<Buddy> buddies,
) {
return ListView.builder(
controller: scrollController,
itemCount: buddies.length,
itemBuilder: (context, index) {
final buddy = buddies[index];
final isSelected = _localSelectedBuddies.any(
(b) => b.buddy.id == buddy.id,
);
final selectedRole = _localSelectedBuddies
.where((b) => b.buddy.id == buddy.id)
.map((b) => b.role)
.firstOrNull;

return ListTile(
leading: CircleAvatar(
backgroundColor: isSelected
? Theme.of(context).colorScheme.primaryContainer
: Theme.of(context).colorScheme.surfaceContainerHighest,
child: isSelected
? Icon(
Icons.check,
color: Theme.of(context).colorScheme.onPrimaryContainer,
)
: Text(
buddy.initials,
style: TextStyle(
fontWeight: FontWeight.bold,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
),
title: Text(buddy.name),
subtitle: buddy.certificationLevel != null
? Text(buddy.certificationLevel!.displayName)
: null,
trailing: isSelected
? Chip(
label: Text(selectedRole?.displayName ?? 'Buddy'),
visualDensity: VisualDensity.compact,
)
: null,
onTap: () {
if (isSelected) {
_removeBuddy(buddy.id);
} else {
_showRoleSelectorForBuddy(context, buddy);
}
},
);
},
);
}

void _removeBuddy(String buddyId) {
setState(() {
_localSelectedBuddies = _localSelectedBuddies
Expand Down
Loading
Loading