refactor: enhance chat page layout and improve drawer search functionality
This commit is contained in:
@@ -1025,47 +1025,87 @@ class _ChatPageState extends ConsumerState<ChatPage> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Row(
|
Transform.translate(
|
||||||
mainAxisSize: MainAxisSize.min,
|
offset: const Offset(-6, 0),
|
||||||
children: [
|
child: Center(
|
||||||
Flexible(
|
child: Row(
|
||||||
child: Text(
|
mainAxisSize: MainAxisSize.min,
|
||||||
_formatModelDisplayName(selectedModel.name),
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
style: AppTypography.headlineSmallStyle
|
children: [
|
||||||
.copyWith(
|
Opacity(
|
||||||
color: context.conduitTheme.textPrimary,
|
opacity: 0.0,
|
||||||
fontWeight: FontWeight.w400,
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: Spacing.xs,
|
||||||
|
vertical: Spacing.xxs,
|
||||||
),
|
),
|
||||||
maxLines: 1,
|
decoration: BoxDecoration(
|
||||||
overflow: TextOverflow.ellipsis,
|
color: context
|
||||||
),
|
.conduitTheme
|
||||||
),
|
.surfaceBackground
|
||||||
const SizedBox(width: Spacing.xs),
|
.withValues(alpha: 0.3),
|
||||||
Container(
|
borderRadius: BorderRadius.circular(
|
||||||
padding: const EdgeInsets.symmetric(
|
AppBorderRadius.badge,
|
||||||
horizontal: Spacing.xs,
|
),
|
||||||
vertical: Spacing.xxs,
|
border: Border.all(
|
||||||
),
|
color:
|
||||||
decoration: BoxDecoration(
|
context.conduitTheme.dividerColor,
|
||||||
color: context.conduitTheme.surfaceBackground
|
width: BorderWidth.thin,
|
||||||
.withValues(alpha: 0.3),
|
),
|
||||||
borderRadius: BorderRadius.circular(
|
),
|
||||||
AppBorderRadius.badge,
|
child: Icon(
|
||||||
|
Platform.isIOS
|
||||||
|
? CupertinoIcons.chevron_down
|
||||||
|
: Icons.keyboard_arrow_down,
|
||||||
|
size: IconSize.small,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
border: Border.all(
|
const SizedBox(width: Spacing.xs),
|
||||||
color: context.conduitTheme.dividerColor,
|
Flexible(
|
||||||
width: BorderWidth.thin,
|
child: Text(
|
||||||
|
_formatModelDisplayName(selectedModel.name),
|
||||||
|
style: AppTypography.headlineSmallStyle
|
||||||
|
.copyWith(
|
||||||
|
color:
|
||||||
|
context.conduitTheme.textPrimary,
|
||||||
|
fontWeight: FontWeight.w400,
|
||||||
|
),
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
const SizedBox(width: Spacing.xs),
|
||||||
child: Icon(
|
Container(
|
||||||
Platform.isIOS
|
padding: const EdgeInsets.symmetric(
|
||||||
? CupertinoIcons.chevron_down
|
horizontal: Spacing.xs,
|
||||||
: Icons.keyboard_arrow_down,
|
vertical: Spacing.xxs,
|
||||||
color: context.conduitTheme.iconSecondary,
|
),
|
||||||
size: IconSize.small,
|
decoration: BoxDecoration(
|
||||||
),
|
color: context
|
||||||
|
.conduitTheme
|
||||||
|
.surfaceBackground
|
||||||
|
.withValues(alpha: 0.3),
|
||||||
|
borderRadius: BorderRadius.circular(
|
||||||
|
AppBorderRadius.badge,
|
||||||
|
),
|
||||||
|
border: Border.all(
|
||||||
|
color: context.conduitTheme.dividerColor,
|
||||||
|
width: BorderWidth.thin,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Icon(
|
||||||
|
Platform.isIOS
|
||||||
|
? CupertinoIcons.chevron_down
|
||||||
|
: Icons.keyboard_arrow_down,
|
||||||
|
color: context.conduitTheme.iconSecondary,
|
||||||
|
size: IconSize.small,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
if (ref.watch(reviewerModeProvider))
|
if (ref.watch(reviewerModeProvider))
|
||||||
Padding(
|
Padding(
|
||||||
@@ -1111,49 +1151,87 @@ class _ChatPageState extends ConsumerState<ChatPage> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Row(
|
Transform.translate(
|
||||||
mainAxisSize: MainAxisSize.min,
|
offset: const Offset(-6, 0),
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
child: Center(
|
||||||
children: [
|
child: Row(
|
||||||
Flexible(
|
mainAxisSize: MainAxisSize.min,
|
||||||
child: Text(
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
'Choose Model',
|
children: [
|
||||||
style: AppTypography.headlineSmallStyle
|
Opacity(
|
||||||
.copyWith(
|
opacity: 0.0,
|
||||||
color: context.conduitTheme.textPrimary,
|
child: Container(
|
||||||
fontWeight: FontWeight.w400,
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: Spacing.xs,
|
||||||
|
vertical: Spacing.xxs,
|
||||||
),
|
),
|
||||||
maxLines: 1,
|
decoration: BoxDecoration(
|
||||||
overflow: TextOverflow.ellipsis,
|
color: context
|
||||||
textAlign: TextAlign.center,
|
.conduitTheme
|
||||||
),
|
.surfaceBackground
|
||||||
),
|
.withValues(alpha: 0.3),
|
||||||
const SizedBox(width: Spacing.xs),
|
borderRadius: BorderRadius.circular(
|
||||||
Container(
|
AppBorderRadius.badge,
|
||||||
padding: const EdgeInsets.symmetric(
|
),
|
||||||
horizontal: Spacing.xs,
|
border: Border.all(
|
||||||
vertical: Spacing.xxs,
|
color:
|
||||||
),
|
context.conduitTheme.dividerColor,
|
||||||
decoration: BoxDecoration(
|
width: BorderWidth.thin,
|
||||||
color: context.conduitTheme.surfaceBackground
|
),
|
||||||
.withValues(alpha: 0.3),
|
),
|
||||||
borderRadius: BorderRadius.circular(
|
child: Icon(
|
||||||
AppBorderRadius.badge,
|
Platform.isIOS
|
||||||
|
? CupertinoIcons.chevron_down
|
||||||
|
: Icons.keyboard_arrow_down,
|
||||||
|
size: IconSize.small,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
border: Border.all(
|
const SizedBox(width: Spacing.xs),
|
||||||
color: context.conduitTheme.dividerColor,
|
Flexible(
|
||||||
width: BorderWidth.thin,
|
child: Text(
|
||||||
|
'Choose Model',
|
||||||
|
style: AppTypography.headlineSmallStyle
|
||||||
|
.copyWith(
|
||||||
|
color:
|
||||||
|
context.conduitTheme.textPrimary,
|
||||||
|
fontWeight: FontWeight.w400,
|
||||||
|
),
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
const SizedBox(width: Spacing.xs),
|
||||||
child: Icon(
|
Container(
|
||||||
Platform.isIOS
|
padding: const EdgeInsets.symmetric(
|
||||||
? CupertinoIcons.chevron_down
|
horizontal: Spacing.xs,
|
||||||
: Icons.keyboard_arrow_down,
|
vertical: Spacing.xxs,
|
||||||
color: context.conduitTheme.iconSecondary,
|
),
|
||||||
size: IconSize.small,
|
decoration: BoxDecoration(
|
||||||
),
|
color: context
|
||||||
|
.conduitTheme
|
||||||
|
.surfaceBackground
|
||||||
|
.withValues(alpha: 0.3),
|
||||||
|
borderRadius: BorderRadius.circular(
|
||||||
|
AppBorderRadius.badge,
|
||||||
|
),
|
||||||
|
border: Border.all(
|
||||||
|
color: context.conduitTheme.dividerColor,
|
||||||
|
width: BorderWidth.thin,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Icon(
|
||||||
|
Platform.isIOS
|
||||||
|
? CupertinoIcons.chevron_down
|
||||||
|
: Icons.keyboard_arrow_down,
|
||||||
|
color: context.conduitTheme.iconSecondary,
|
||||||
|
size: IconSize.small,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
if (ref.watch(reviewerModeProvider))
|
if (ref.watch(reviewerModeProvider))
|
||||||
Padding(
|
Padding(
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ class ChatsDrawer extends ConsumerStatefulWidget {
|
|||||||
class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
|
class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
|
||||||
final TextEditingController _searchController = TextEditingController();
|
final TextEditingController _searchController = TextEditingController();
|
||||||
final FocusNode _searchFocusNode = FocusNode(debugLabel: 'drawer_search');
|
final FocusNode _searchFocusNode = FocusNode(debugLabel: 'drawer_search');
|
||||||
|
final ScrollController _listController = ScrollController();
|
||||||
Timer? _debounce;
|
Timer? _debounce;
|
||||||
String _query = '';
|
String _query = '';
|
||||||
bool _isLoadingConversation = false;
|
bool _isLoadingConversation = false;
|
||||||
@@ -44,6 +45,7 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
|
|||||||
_debounce?.cancel();
|
_debounce?.cancel();
|
||||||
_searchController.dispose();
|
_searchController.dispose();
|
||||||
_searchFocusNode.dispose();
|
_searchFocusNode.dispose();
|
||||||
|
_listController.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,18 +88,23 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
|
|||||||
? CupertinoIcons.bubble_left
|
? CupertinoIcons.bubble_left
|
||||||
: Icons.add_comment,
|
: Icons.add_comment,
|
||||||
color: theme.iconPrimary,
|
color: theme.iconPrimary,
|
||||||
|
size: IconSize.listItem,
|
||||||
),
|
),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
chat.startNewChat(ref);
|
chat.startNewChat(ref);
|
||||||
if (mounted) Navigator.of(context).maybePop();
|
if (mounted) Navigator.of(context).maybePop();
|
||||||
},
|
},
|
||||||
tooltip: AppLocalizations.of(context)!.newChat,
|
tooltip: AppLocalizations.of(context)!.newChat,
|
||||||
|
constraints: const BoxConstraints(
|
||||||
|
minWidth: TouchTarget.listItem,
|
||||||
|
minHeight: TouchTarget.listItem,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Expanded(child: _buildConversationList(context)),
|
Expanded(child: _buildConversationList(context)),
|
||||||
const Divider(height: 1),
|
Divider(height: 1, color: theme.dividerColor),
|
||||||
_buildBottomSection(context),
|
_buildBottomSection(context),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -106,64 +113,58 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
|
|||||||
|
|
||||||
Widget _buildSearchField(BuildContext context) {
|
Widget _buildSearchField(BuildContext context) {
|
||||||
final theme = context.conduitTheme;
|
final theme = context.conduitTheme;
|
||||||
return Container(
|
return TextField(
|
||||||
decoration: BoxDecoration(
|
controller: _searchController,
|
||||||
gradient: LinearGradient(
|
focusNode: _searchFocusNode,
|
||||||
colors: [
|
onChanged: (_) => _onSearchChanged(),
|
||||||
theme.inputBackground.withValues(alpha: 0.6),
|
style: TextStyle(
|
||||||
theme.inputBackground.withValues(alpha: 0.3),
|
color: theme.inputText,
|
||||||
],
|
fontSize: AppTypography.bodyMedium,
|
||||||
begin: Alignment.topLeft,
|
|
||||||
end: Alignment.bottomRight,
|
|
||||||
),
|
|
||||||
borderRadius: BorderRadius.circular(AppBorderRadius.lg),
|
|
||||||
border: Border.all(
|
|
||||||
color: _searchFocusNode.hasFocus
|
|
||||||
? theme.buttonPrimary.withValues(alpha: 0.8)
|
|
||||||
: theme.inputBorder.withValues(alpha: 0.3),
|
|
||||||
width: BorderWidth.thin,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
child: TextField(
|
decoration: InputDecoration(
|
||||||
controller: _searchController,
|
hintText: AppLocalizations.of(context)!.searchConversations,
|
||||||
focusNode: _searchFocusNode,
|
hintStyle: TextStyle(
|
||||||
onChanged: (_) => _onSearchChanged(),
|
color: theme.inputPlaceholder,
|
||||||
style: TextStyle(
|
|
||||||
color: theme.inputText,
|
|
||||||
fontSize: AppTypography.bodyMedium,
|
fontSize: AppTypography.bodyMedium,
|
||||||
),
|
),
|
||||||
decoration: InputDecoration(
|
prefixIcon: Icon(
|
||||||
hintText: AppLocalizations.of(context)!.searchConversations,
|
Platform.isIOS ? CupertinoIcons.search : Icons.search,
|
||||||
hintStyle: TextStyle(
|
color: theme.iconSecondary,
|
||||||
color: theme.inputPlaceholder.withValues(alpha: 0.8),
|
size: IconSize.input,
|
||||||
fontSize: AppTypography.bodyMedium,
|
),
|
||||||
),
|
suffixIcon: _query.isNotEmpty
|
||||||
prefixIcon: Icon(
|
? IconButton(
|
||||||
Platform.isIOS ? CupertinoIcons.search : Icons.search,
|
onPressed: () {
|
||||||
color: theme.iconSecondary,
|
_searchController.clear();
|
||||||
size: IconSize.md,
|
setState(() => _query = '');
|
||||||
),
|
_searchFocusNode.unfocus();
|
||||||
suffixIcon: _query.isNotEmpty
|
},
|
||||||
? IconButton(
|
icon: Icon(
|
||||||
onPressed: () {
|
Platform.isIOS
|
||||||
_searchController.clear();
|
? CupertinoIcons.clear_circled_solid
|
||||||
setState(() => _query = '');
|
: Icons.clear,
|
||||||
_searchFocusNode.unfocus();
|
color: theme.iconSecondary,
|
||||||
},
|
size: IconSize.input,
|
||||||
icon: Icon(
|
),
|
||||||
Platform.isIOS
|
)
|
||||||
? CupertinoIcons.clear_circled_solid
|
: null,
|
||||||
: Icons.clear,
|
filled: true,
|
||||||
color: theme.iconSecondary,
|
fillColor: theme.inputBackground,
|
||||||
size: IconSize.md,
|
border: OutlineInputBorder(
|
||||||
),
|
borderRadius: BorderRadius.circular(AppBorderRadius.md),
|
||||||
)
|
borderSide: BorderSide.none,
|
||||||
: null,
|
),
|
||||||
border: InputBorder.none,
|
enabledBorder: OutlineInputBorder(
|
||||||
contentPadding: const EdgeInsets.symmetric(
|
borderRadius: BorderRadius.circular(AppBorderRadius.md),
|
||||||
horizontal: 16,
|
borderSide: BorderSide(color: theme.inputBorder, width: 1),
|
||||||
vertical: 12,
|
),
|
||||||
),
|
focusedBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(AppBorderRadius.md),
|
||||||
|
borderSide: BorderSide(color: theme.buttonPrimary, width: 1),
|
||||||
|
),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: Spacing.md,
|
||||||
|
vertical: Spacing.md,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -213,87 +214,93 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
|
|||||||
.toList();
|
.toList();
|
||||||
final archived = list.where((c) => c.archived == true).toList();
|
final archived = list.where((c) => c.archived == true).toList();
|
||||||
|
|
||||||
return ListView(
|
return Scrollbar(
|
||||||
padding: const EdgeInsets.fromLTRB(
|
controller: _listController,
|
||||||
Spacing.md,
|
child: ListView(
|
||||||
Spacing.sm,
|
controller: _listController,
|
||||||
Spacing.md,
|
padding: const EdgeInsets.fromLTRB(
|
||||||
Spacing.md,
|
Spacing.md,
|
||||||
),
|
Spacing.sm,
|
||||||
children: [
|
Spacing.md,
|
||||||
if (pinned.isNotEmpty) ...[
|
Spacing.md,
|
||||||
_buildSectionHeader(
|
),
|
||||||
AppLocalizations.of(context)!.pinned,
|
children: [
|
||||||
pinned.length,
|
if (pinned.isNotEmpty) ...[
|
||||||
),
|
_buildSectionHeader(
|
||||||
const SizedBox(height: Spacing.xs),
|
AppLocalizations.of(context)!.pinned,
|
||||||
...pinned.map((conv) => _buildTileFor(conv)),
|
pinned.length,
|
||||||
const SizedBox(height: Spacing.md),
|
|
||||||
],
|
|
||||||
|
|
||||||
// Folders section (shown even if empty)
|
|
||||||
_buildFoldersSectionHeader(),
|
|
||||||
const SizedBox(height: Spacing.xs),
|
|
||||||
if (_isDragging && _draggingHasFolder) ...[
|
|
||||||
_buildUnfileDropTarget(),
|
|
||||||
const SizedBox(height: Spacing.sm),
|
|
||||||
],
|
|
||||||
...ref
|
|
||||||
.watch(foldersProvider)
|
|
||||||
.when(
|
|
||||||
data: (folders) {
|
|
||||||
final grouped = <String, List<dynamic>>{};
|
|
||||||
for (final c in foldered) {
|
|
||||||
final id = c.folderId!;
|
|
||||||
grouped.putIfAbsent(id, () => []).add(c);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show all folders (including empty)
|
|
||||||
final sections = folders.map((folder) {
|
|
||||||
final expandedMap = ref.watch(_expandedFoldersProvider);
|
|
||||||
final isExpanded = expandedMap[folder.id] ?? false;
|
|
||||||
final convs = grouped[folder.id] ?? const <dynamic>[];
|
|
||||||
return Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
||||||
children: [
|
|
||||||
_buildFolderHeader(
|
|
||||||
folder.id,
|
|
||||||
folder.name,
|
|
||||||
convs.length,
|
|
||||||
),
|
|
||||||
if (isExpanded && convs.isNotEmpty) ...[
|
|
||||||
const SizedBox(height: Spacing.xs),
|
|
||||||
...convs.map(
|
|
||||||
(c) => _buildTileFor(c, inFolder: true),
|
|
||||||
),
|
|
||||||
const SizedBox(height: Spacing.sm),
|
|
||||||
],
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}).toList();
|
|
||||||
return sections.isEmpty
|
|
||||||
? [const SizedBox.shrink()]
|
|
||||||
: sections;
|
|
||||||
},
|
|
||||||
loading: () => [const SizedBox.shrink()],
|
|
||||||
error: (e, st) => [const SizedBox.shrink()],
|
|
||||||
),
|
),
|
||||||
const SizedBox(height: Spacing.md),
|
const SizedBox(height: Spacing.xs),
|
||||||
|
...pinned.map((conv) => _buildTileFor(conv)),
|
||||||
|
const SizedBox(height: Spacing.md),
|
||||||
|
],
|
||||||
|
|
||||||
if (regular.isNotEmpty) ...[
|
// Folders section (shown even if empty)
|
||||||
_buildSectionHeader(
|
_buildFoldersSectionHeader(),
|
||||||
AppLocalizations.of(context)!.recent,
|
|
||||||
regular.length,
|
|
||||||
),
|
|
||||||
const SizedBox(height: Spacing.xs),
|
const SizedBox(height: Spacing.xs),
|
||||||
...regular.map(_buildTileFor),
|
if (_isDragging && _draggingHasFolder) ...[
|
||||||
],
|
_buildUnfileDropTarget(),
|
||||||
|
const SizedBox(height: Spacing.sm),
|
||||||
|
],
|
||||||
|
...ref
|
||||||
|
.watch(foldersProvider)
|
||||||
|
.when(
|
||||||
|
data: (folders) {
|
||||||
|
final grouped = <String, List<dynamic>>{};
|
||||||
|
for (final c in foldered) {
|
||||||
|
final id = c.folderId!;
|
||||||
|
grouped.putIfAbsent(id, () => []).add(c);
|
||||||
|
}
|
||||||
|
|
||||||
if (archived.isNotEmpty) ...[
|
// Show all folders (including empty)
|
||||||
|
final sections = folders.map((folder) {
|
||||||
|
final expandedMap = ref.watch(
|
||||||
|
_expandedFoldersProvider,
|
||||||
|
);
|
||||||
|
final isExpanded = expandedMap[folder.id] ?? false;
|
||||||
|
final convs = grouped[folder.id] ?? const <dynamic>[];
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
_buildFolderHeader(
|
||||||
|
folder.id,
|
||||||
|
folder.name,
|
||||||
|
convs.length,
|
||||||
|
),
|
||||||
|
if (isExpanded && convs.isNotEmpty) ...[
|
||||||
|
const SizedBox(height: Spacing.xs),
|
||||||
|
...convs.map(
|
||||||
|
(c) => _buildTileFor(c, inFolder: true),
|
||||||
|
),
|
||||||
|
const SizedBox(height: Spacing.sm),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}).toList();
|
||||||
|
return sections.isEmpty
|
||||||
|
? [const SizedBox.shrink()]
|
||||||
|
: sections;
|
||||||
|
},
|
||||||
|
loading: () => [const SizedBox.shrink()],
|
||||||
|
error: (e, st) => [const SizedBox.shrink()],
|
||||||
|
),
|
||||||
const SizedBox(height: Spacing.md),
|
const SizedBox(height: Spacing.md),
|
||||||
_buildArchivedSection(archived),
|
|
||||||
|
if (regular.isNotEmpty) ...[
|
||||||
|
_buildSectionHeader(
|
||||||
|
AppLocalizations.of(context)!.recent,
|
||||||
|
regular.length,
|
||||||
|
),
|
||||||
|
const SizedBox(height: Spacing.xs),
|
||||||
|
...regular.map(_buildTileFor),
|
||||||
|
],
|
||||||
|
|
||||||
|
if (archived.isNotEmpty) ...[
|
||||||
|
const SizedBox(height: Spacing.md),
|
||||||
|
_buildArchivedSection(archived),
|
||||||
|
],
|
||||||
],
|
],
|
||||||
],
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
loading: () =>
|
loading: () =>
|
||||||
@@ -350,85 +357,89 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
|
|||||||
.toList();
|
.toList();
|
||||||
final archived = list.where((c) => c.archived == true).toList();
|
final archived = list.where((c) => c.archived == true).toList();
|
||||||
|
|
||||||
return ListView(
|
return Scrollbar(
|
||||||
padding: const EdgeInsets.fromLTRB(
|
controller: _listController,
|
||||||
Spacing.md,
|
child: ListView(
|
||||||
Spacing.sm,
|
controller: _listController,
|
||||||
Spacing.md,
|
padding: const EdgeInsets.fromLTRB(
|
||||||
Spacing.md,
|
Spacing.md,
|
||||||
),
|
Spacing.sm,
|
||||||
children: [
|
Spacing.md,
|
||||||
_buildSectionHeader('Results', list.length),
|
Spacing.md,
|
||||||
const SizedBox(height: Spacing.xs),
|
),
|
||||||
if (pinned.isNotEmpty) ...[
|
children: [
|
||||||
_buildSectionHeader(
|
_buildSectionHeader('Results', list.length),
|
||||||
AppLocalizations.of(context)!.pinned,
|
|
||||||
pinned.length,
|
|
||||||
),
|
|
||||||
const SizedBox(height: Spacing.xs),
|
const SizedBox(height: Spacing.xs),
|
||||||
...pinned.map((conv) => _buildTileFor(conv)),
|
if (pinned.isNotEmpty) ...[
|
||||||
const SizedBox(height: Spacing.md),
|
_buildSectionHeader(
|
||||||
],
|
AppLocalizations.of(context)!.pinned,
|
||||||
// Folders section (shown even if empty)
|
pinned.length,
|
||||||
_buildFoldersSectionHeader(),
|
|
||||||
const SizedBox(height: Spacing.xs),
|
|
||||||
if (_isDragging && _draggingHasFolder) ...[
|
|
||||||
_buildUnfileDropTarget(),
|
|
||||||
const SizedBox(height: Spacing.sm),
|
|
||||||
],
|
|
||||||
...ref
|
|
||||||
.watch(foldersProvider)
|
|
||||||
.when(
|
|
||||||
data: (folders) {
|
|
||||||
final grouped = <String, List<dynamic>>{};
|
|
||||||
for (final c in foldered) {
|
|
||||||
final id = c.folderId!;
|
|
||||||
grouped.putIfAbsent(id, () => []).add(c);
|
|
||||||
}
|
|
||||||
|
|
||||||
final sections = folders.map((folder) {
|
|
||||||
final expandedMap = ref.watch(_expandedFoldersProvider);
|
|
||||||
final isExpanded = expandedMap[folder.id] ?? false;
|
|
||||||
final convs = grouped[folder.id] ?? const <dynamic>[];
|
|
||||||
return Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
||||||
children: [
|
|
||||||
_buildFolderHeader(
|
|
||||||
folder.id,
|
|
||||||
folder.name,
|
|
||||||
convs.length,
|
|
||||||
),
|
|
||||||
if (isExpanded && convs.isNotEmpty) ...[
|
|
||||||
const SizedBox(height: Spacing.xs),
|
|
||||||
...convs.map(
|
|
||||||
(c) => _buildTileFor(c, inFolder: true),
|
|
||||||
),
|
|
||||||
const SizedBox(height: Spacing.sm),
|
|
||||||
],
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}).toList();
|
|
||||||
return sections.isEmpty
|
|
||||||
? [const SizedBox.shrink()]
|
|
||||||
: sections;
|
|
||||||
},
|
|
||||||
loading: () => [const SizedBox.shrink()],
|
|
||||||
error: (e, st) => [const SizedBox.shrink()],
|
|
||||||
),
|
),
|
||||||
const SizedBox(height: Spacing.md),
|
const SizedBox(height: Spacing.xs),
|
||||||
if (regular.isNotEmpty) ...[
|
...pinned.map((conv) => _buildTileFor(conv)),
|
||||||
_buildSectionHeader(
|
const SizedBox(height: Spacing.md),
|
||||||
AppLocalizations.of(context)!.recent,
|
],
|
||||||
regular.length,
|
// Folders section (shown even if empty)
|
||||||
),
|
_buildFoldersSectionHeader(),
|
||||||
const SizedBox(height: Spacing.xs),
|
const SizedBox(height: Spacing.xs),
|
||||||
...regular.map(_buildTileFor),
|
if (_isDragging && _draggingHasFolder) ...[
|
||||||
],
|
_buildUnfileDropTarget(),
|
||||||
if (archived.isNotEmpty) ...[
|
const SizedBox(height: Spacing.sm),
|
||||||
|
],
|
||||||
|
...ref
|
||||||
|
.watch(foldersProvider)
|
||||||
|
.when(
|
||||||
|
data: (folders) {
|
||||||
|
final grouped = <String, List<dynamic>>{};
|
||||||
|
for (final c in foldered) {
|
||||||
|
final id = c.folderId!;
|
||||||
|
grouped.putIfAbsent(id, () => []).add(c);
|
||||||
|
}
|
||||||
|
|
||||||
|
final sections = folders.map((folder) {
|
||||||
|
final expandedMap = ref.watch(_expandedFoldersProvider);
|
||||||
|
final isExpanded = expandedMap[folder.id] ?? false;
|
||||||
|
final convs = grouped[folder.id] ?? const <dynamic>[];
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
_buildFolderHeader(
|
||||||
|
folder.id,
|
||||||
|
folder.name,
|
||||||
|
convs.length,
|
||||||
|
),
|
||||||
|
if (isExpanded && convs.isNotEmpty) ...[
|
||||||
|
const SizedBox(height: Spacing.xs),
|
||||||
|
...convs.map(
|
||||||
|
(c) => _buildTileFor(c, inFolder: true),
|
||||||
|
),
|
||||||
|
const SizedBox(height: Spacing.sm),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}).toList();
|
||||||
|
return sections.isEmpty
|
||||||
|
? [const SizedBox.shrink()]
|
||||||
|
: sections;
|
||||||
|
},
|
||||||
|
loading: () => [const SizedBox.shrink()],
|
||||||
|
error: (e, st) => [const SizedBox.shrink()],
|
||||||
|
),
|
||||||
const SizedBox(height: Spacing.md),
|
const SizedBox(height: Spacing.md),
|
||||||
_buildArchivedSection(archived),
|
if (regular.isNotEmpty) ...[
|
||||||
|
_buildSectionHeader(
|
||||||
|
AppLocalizations.of(context)!.recent,
|
||||||
|
regular.length,
|
||||||
|
),
|
||||||
|
const SizedBox(height: Spacing.xs),
|
||||||
|
...regular.map(_buildTileFor),
|
||||||
|
],
|
||||||
|
if (archived.isNotEmpty) ...[
|
||||||
|
const SizedBox(height: Spacing.md),
|
||||||
|
_buildArchivedSection(archived),
|
||||||
|
],
|
||||||
],
|
],
|
||||||
],
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
loading: () =>
|
loading: () =>
|
||||||
@@ -453,17 +464,13 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
|
|||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
title,
|
title,
|
||||||
style: AppTypography.bodySmallStyle.copyWith(
|
style: AppTypography.labelStyle.copyWith(color: theme.textSecondary),
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
color: theme.textSecondary,
|
|
||||||
letterSpacing: 0.2,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
const SizedBox(width: Spacing.xs),
|
const SizedBox(width: Spacing.xs),
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: theme.surfaceBackground.withValues(alpha: 0.6),
|
color: theme.surfaceContainer.withValues(alpha: 0.6),
|
||||||
borderRadius: BorderRadius.circular(AppBorderRadius.xs),
|
borderRadius: BorderRadius.circular(AppBorderRadius.xs),
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
color: theme.dividerColor,
|
color: theme.dividerColor,
|
||||||
@@ -472,9 +479,7 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
|
|||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
'$count',
|
'$count',
|
||||||
style: AppTypography.bodySmallStyle.copyWith(
|
style: AppTypography.tiny.copyWith(color: theme.textSecondary),
|
||||||
color: theme.textSecondary,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -488,11 +493,7 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
|
|||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
AppLocalizations.of(context)!.folders,
|
AppLocalizations.of(context)!.folders,
|
||||||
style: AppTypography.bodySmallStyle.copyWith(
|
style: AppTypography.labelStyle.copyWith(color: theme.textSecondary),
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
color: theme.textSecondary,
|
|
||||||
letterSpacing: 0.2,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
IconButton(
|
IconButton(
|
||||||
@@ -615,7 +616,7 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
|
|||||||
return Material(
|
return Material(
|
||||||
color: isHover
|
color: isHover
|
||||||
? theme.buttonPrimary.withValues(alpha: 0.08)
|
? theme.buttonPrimary.withValues(alpha: 0.08)
|
||||||
: theme.surfaceBackground.withValues(alpha: 0.05),
|
: theme.surfaceContainer.withValues(alpha: 0.05),
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(AppBorderRadius.md),
|
borderRadius: BorderRadius.circular(AppBorderRadius.md),
|
||||||
side: BorderSide(
|
side: BorderSide(
|
||||||
@@ -652,6 +653,7 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
|
|||||||
? CupertinoIcons.folder
|
? CupertinoIcons.folder
|
||||||
: Icons.folder),
|
: Icons.folder),
|
||||||
color: theme.iconPrimary,
|
color: theme.iconPrimary,
|
||||||
|
size: IconSize.listItem,
|
||||||
),
|
),
|
||||||
const SizedBox(width: Spacing.sm),
|
const SizedBox(width: Spacing.sm),
|
||||||
Expanded(
|
Expanded(
|
||||||
@@ -679,6 +681,7 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
|
|||||||
? CupertinoIcons.chevron_down
|
? CupertinoIcons.chevron_down
|
||||||
: Icons.expand_more),
|
: Icons.expand_more),
|
||||||
color: theme.iconSecondary,
|
color: theme.iconSecondary,
|
||||||
|
size: IconSize.listItem,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -890,7 +893,7 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
|
|||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: isHover
|
color: isHover
|
||||||
? theme.buttonPrimary.withValues(alpha: 0.08)
|
? theme.buttonPrimary.withValues(alpha: 0.08)
|
||||||
: theme.surfaceBackground.withValues(alpha: 0.03),
|
: theme.surfaceContainer.withValues(alpha: 0.03),
|
||||||
borderRadius: BorderRadius.circular(AppBorderRadius.md),
|
borderRadius: BorderRadius.circular(AppBorderRadius.md),
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
color: isHover
|
color: isHover
|
||||||
@@ -931,6 +934,7 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
|
|||||||
Widget _buildTileFor(dynamic conv, {bool inFolder = false}) {
|
Widget _buildTileFor(dynamic conv, {bool inFolder = false}) {
|
||||||
final isActive = ref.watch(activeConversationProvider)?.id == conv.id;
|
final isActive = ref.watch(activeConversationProvider)?.id == conv.id;
|
||||||
final title = conv.title?.isEmpty == true ? 'Chat' : (conv.title ?? 'Chat');
|
final title = conv.title?.isEmpty == true ? 'Chat' : (conv.title ?? 'Chat');
|
||||||
|
final theme = context.conduitTheme;
|
||||||
final tile = _ConversationTile(
|
final tile = _ConversationTile(
|
||||||
title: title,
|
title: title,
|
||||||
pinned: conv.pinned == true,
|
pinned: conv.pinned == true,
|
||||||
@@ -938,14 +942,12 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
|
|||||||
onTap: _isLoadingConversation
|
onTap: _isLoadingConversation
|
||||||
? null
|
? null
|
||||||
: () => _selectConversation(context, conv.id),
|
: () => _selectConversation(context, conv.id),
|
||||||
// Remove long-press context menu to avoid conflict with drag gesture
|
|
||||||
onLongPress: null,
|
onLongPress: null,
|
||||||
onMorePressed: () {
|
onMorePressed: () {
|
||||||
HapticFeedback.selectionClick();
|
HapticFeedback.selectionClick();
|
||||||
_showConversationContextMenu(context, conv);
|
_showConversationContextMenu(context, conv);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: EdgeInsets.only(
|
padding: EdgeInsets.only(
|
||||||
bottom: Spacing.xs,
|
bottom: Spacing.xs,
|
||||||
@@ -966,12 +968,13 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
|
|||||||
vertical: Spacing.sm,
|
vertical: Spacing.sm,
|
||||||
),
|
),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Theme.of(context).cardColor,
|
color: theme.cardBackground,
|
||||||
borderRadius: BorderRadius.circular(AppBorderRadius.md),
|
borderRadius: BorderRadius.circular(AppBorderRadius.md),
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
color: Theme.of(context).dividerColor,
|
color: theme.dividerColor,
|
||||||
width: BorderWidth.regular,
|
width: BorderWidth.regular,
|
||||||
),
|
),
|
||||||
|
boxShadow: ConduitShadows.card,
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
@@ -980,7 +983,7 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
|
|||||||
Platform.isIOS
|
Platform.isIOS
|
||||||
? CupertinoIcons.chat_bubble_2
|
? CupertinoIcons.chat_bubble_2
|
||||||
: Icons.chat_bubble_outline,
|
: Icons.chat_bubble_outline,
|
||||||
size: IconSize.md,
|
size: IconSize.listItem,
|
||||||
),
|
),
|
||||||
const SizedBox(width: Spacing.xs),
|
const SizedBox(width: Spacing.xs),
|
||||||
Text(title, maxLines: 1, overflow: TextOverflow.ellipsis),
|
Text(title, maxLines: 1, overflow: TextOverflow.ellipsis),
|
||||||
@@ -1019,7 +1022,7 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
Material(
|
Material(
|
||||||
color: theme.surfaceBackground.withValues(alpha: 0.05),
|
color: theme.surfaceContainer.withValues(alpha: 0.05),
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(AppBorderRadius.md),
|
borderRadius: BorderRadius.circular(AppBorderRadius.md),
|
||||||
side: BorderSide(
|
side: BorderSide(
|
||||||
@@ -1042,6 +1045,7 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
|
|||||||
? CupertinoIcons.archivebox
|
? CupertinoIcons.archivebox
|
||||||
: Icons.archive_rounded,
|
: Icons.archive_rounded,
|
||||||
color: theme.iconPrimary,
|
color: theme.iconPrimary,
|
||||||
|
size: IconSize.listItem,
|
||||||
),
|
),
|
||||||
const SizedBox(width: Spacing.sm),
|
const SizedBox(width: Spacing.sm),
|
||||||
Expanded(
|
Expanded(
|
||||||
@@ -1069,6 +1073,7 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
|
|||||||
? CupertinoIcons.chevron_down
|
? CupertinoIcons.chevron_down
|
||||||
: Icons.expand_more),
|
: Icons.expand_more),
|
||||||
color: theme.iconSecondary,
|
color: theme.iconSecondary,
|
||||||
|
size: IconSize.listItem,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -1179,12 +1184,13 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
|
|||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.all(Spacing.sm),
|
padding: const EdgeInsets.all(Spacing.sm),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: theme.surfaceBackground.withValues(alpha: 0.04),
|
color: theme.surfaceContainer.withValues(alpha: 0.05),
|
||||||
borderRadius: BorderRadius.circular(AppBorderRadius.md),
|
borderRadius: BorderRadius.circular(AppBorderRadius.md),
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
color: theme.dividerColor,
|
color: theme.dividerColor,
|
||||||
width: BorderWidth.regular,
|
width: BorderWidth.regular,
|
||||||
),
|
),
|
||||||
|
boxShadow: ConduitShadows.card,
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
@@ -1506,9 +1512,7 @@ class _ConversationTile extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = context.conduitTheme;
|
final theme = context.conduitTheme;
|
||||||
return Material(
|
return Material(
|
||||||
color: selected
|
color: Colors.transparent,
|
||||||
? theme.buttonPrimary.withValues(alpha: 0.08)
|
|
||||||
: theme.surfaceBackground.withValues(alpha: 0.03),
|
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(AppBorderRadius.md),
|
borderRadius: BorderRadius.circular(AppBorderRadius.md),
|
||||||
side: BorderSide(
|
side: BorderSide(
|
||||||
@@ -1534,7 +1538,7 @@ class _ConversationTile extends StatelessWidget {
|
|||||||
title,
|
title,
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
style: AppTypography.bodyLargeStyle.copyWith(
|
style: AppTypography.standard.copyWith(
|
||||||
color: theme.textPrimary,
|
color: theme.textPrimary,
|
||||||
fontWeight: selected ? FontWeight.w600 : FontWeight.w500,
|
fontWeight: selected ? FontWeight.w600 : FontWeight.w500,
|
||||||
),
|
),
|
||||||
@@ -1546,15 +1550,15 @@ class _ConversationTile extends StatelessWidget {
|
|||||||
visualDensity: VisualDensity.compact,
|
visualDensity: VisualDensity.compact,
|
||||||
padding: EdgeInsets.zero,
|
padding: EdgeInsets.zero,
|
||||||
constraints: const BoxConstraints(
|
constraints: const BoxConstraints(
|
||||||
minWidth: 36,
|
minWidth: TouchTarget.listItem,
|
||||||
minHeight: 36,
|
minHeight: TouchTarget.listItem,
|
||||||
),
|
),
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
Platform.isIOS
|
Platform.isIOS
|
||||||
? CupertinoIcons.ellipsis
|
? CupertinoIcons.ellipsis
|
||||||
: Icons.more_vert_rounded,
|
: Icons.more_vert_rounded,
|
||||||
color: theme.iconSecondary,
|
color: theme.iconSecondary,
|
||||||
size: IconSize.md,
|
size: IconSize.listItem,
|
||||||
),
|
),
|
||||||
onPressed: onMorePressed,
|
onPressed: onMorePressed,
|
||||||
tooltip: AppLocalizations.of(context)!.more,
|
tooltip: AppLocalizations.of(context)!.more,
|
||||||
|
|||||||
Reference in New Issue
Block a user