feat: redesign chats drawer to make it more minimal
This commit is contained in:
@@ -190,12 +190,6 @@ final apiServiceProvider = Provider<ApiService?>((ref) {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Initialize with any existing token immediately
|
|
||||||
final token = ref.read(authTokenProvider3);
|
|
||||||
if (token != null && token.isNotEmpty) {
|
|
||||||
apiService.updateAuthToken(token);
|
|
||||||
}
|
|
||||||
|
|
||||||
return apiService;
|
return apiService;
|
||||||
},
|
},
|
||||||
orElse: () => null,
|
orElse: () => null,
|
||||||
|
|||||||
@@ -1010,7 +1010,7 @@ class _ChatPageState extends ConsumerState<ChatPage> {
|
|||||||
drawerEdgeDragWidth: MediaQuery.of(context).size.width * 0.5,
|
drawerEdgeDragWidth: MediaQuery.of(context).size.width * 0.5,
|
||||||
drawerScrimColor: Colors.black.withValues(alpha: 0.32),
|
drawerScrimColor: Colors.black.withValues(alpha: 0.32),
|
||||||
drawer: Drawer(
|
drawer: Drawer(
|
||||||
width: (MediaQuery.of(context).size.width * 0.88).clamp(
|
width: (MediaQuery.of(context).size.width * 0.80).clamp(
|
||||||
280.0,
|
280.0,
|
||||||
420.0,
|
420.0,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -331,9 +331,6 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
|
|||||||
final Brightness brightness = Theme.of(context).brightness;
|
final Brightness brightness = Theme.of(context).brightness;
|
||||||
final bool isActive = _focusNode.hasFocus || _hasText;
|
final bool isActive = _focusNode.hasFocus || _hasText;
|
||||||
final Color composerSurface = context.conduitTheme.inputBackground;
|
final Color composerSurface = context.conduitTheme.inputBackground;
|
||||||
final Color shellBackground = brightness == Brightness.dark
|
|
||||||
? composerSurface.withValues(alpha: 0.78)
|
|
||||||
: composerSurface;
|
|
||||||
final Color placeholderBase = context.conduitTheme.inputPlaceholder;
|
final Color placeholderBase = context.conduitTheme.inputPlaceholder;
|
||||||
final Color placeholderFocused = context.conduitTheme.inputText.withValues(
|
final Color placeholderFocused = context.conduitTheme.inputText.withValues(
|
||||||
alpha: 0.64,
|
alpha: 0.64,
|
||||||
@@ -429,7 +426,9 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
|
|||||||
duration: const Duration(milliseconds: 180),
|
duration: const Duration(milliseconds: 180),
|
||||||
curve: Curves.easeOutCubic,
|
curve: Curves.easeOutCubic,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: shellBackground,
|
color: brightness == Brightness.dark
|
||||||
|
? composerSurface.withValues(alpha: 0.78)
|
||||||
|
: composerSurface,
|
||||||
borderRadius: BorderRadius.circular(_composerRadius),
|
borderRadius: BorderRadius.circular(_composerRadius),
|
||||||
border: Border.all(color: outlineColor, width: BorderWidth.thin),
|
border: Border.all(color: outlineColor, width: BorderWidth.thin),
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
@@ -465,7 +464,7 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
|
|||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.all(Spacing.sm),
|
padding: const EdgeInsets.all(Spacing.sm),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: shellBackground,
|
color: Colors.transparent,
|
||||||
borderRadius: BorderRadius.circular(_composerRadius),
|
borderRadius: BorderRadius.circular(_composerRadius),
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
@@ -600,14 +599,17 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
|
|||||||
? FontStyle.italic
|
? FontStyle.italic
|
||||||
: FontStyle.normal,
|
: FontStyle.normal,
|
||||||
),
|
),
|
||||||
filled: true,
|
filled: false,
|
||||||
fillColor: shellBackground,
|
|
||||||
border: InputBorder.none,
|
border: InputBorder.none,
|
||||||
enabledBorder: InputBorder.none,
|
enabledBorder: InputBorder.none,
|
||||||
focusedBorder: InputBorder.none,
|
focusedBorder: InputBorder.none,
|
||||||
errorBorder: InputBorder.none,
|
errorBorder: InputBorder.none,
|
||||||
disabledBorder: InputBorder.none,
|
disabledBorder: InputBorder.none,
|
||||||
contentPadding: EdgeInsets.zero,
|
contentPadding:
|
||||||
|
const EdgeInsets.symmetric(
|
||||||
|
horizontal: Spacing.sm,
|
||||||
|
vertical: Spacing.sm,
|
||||||
|
),
|
||||||
isDense: true,
|
isDense: true,
|
||||||
alignLabelWithHint: true,
|
alignLabelWithHint: true,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -70,12 +70,7 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
|
|||||||
|
|
||||||
Widget _buildRefreshableScrollable({required List<Widget> children}) {
|
Widget _buildRefreshableScrollable({required List<Widget> children}) {
|
||||||
// Common padding used in both scrollable variants
|
// Common padding used in both scrollable variants
|
||||||
const padding = EdgeInsets.fromLTRB(
|
const padding = EdgeInsets.fromLTRB(0, Spacing.sm, 0, Spacing.md);
|
||||||
Spacing.md,
|
|
||||||
Spacing.sm,
|
|
||||||
Spacing.md,
|
|
||||||
Spacing.md,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (Platform.isIOS) {
|
if (Platform.isIOS) {
|
||||||
// Use Cupertino-style pull-to-refresh on iOS
|
// Use Cupertino-style pull-to-refresh on iOS
|
||||||
@@ -187,21 +182,22 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
|
|||||||
controller: _searchController,
|
controller: _searchController,
|
||||||
focusNode: _searchFocusNode,
|
focusNode: _searchFocusNode,
|
||||||
onChanged: (_) => _onSearchChanged(),
|
onChanged: (_) => _onSearchChanged(),
|
||||||
style: TextStyle(
|
style: AppTypography.standard.copyWith(color: theme.inputText),
|
||||||
color: theme.inputText,
|
|
||||||
fontSize: AppTypography.bodyMedium,
|
|
||||||
),
|
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
|
isDense: true,
|
||||||
hintText: AppLocalizations.of(context)!.searchConversations,
|
hintText: AppLocalizations.of(context)!.searchConversations,
|
||||||
hintStyle: TextStyle(
|
hintStyle: AppTypography.standard.copyWith(
|
||||||
color: theme.inputPlaceholder,
|
color: theme.inputPlaceholder,
|
||||||
fontSize: AppTypography.bodyMedium,
|
|
||||||
),
|
),
|
||||||
prefixIcon: Icon(
|
prefixIcon: Icon(
|
||||||
Platform.isIOS ? CupertinoIcons.search : Icons.search,
|
Platform.isIOS ? CupertinoIcons.search : Icons.search,
|
||||||
color: theme.iconSecondary,
|
color: theme.iconSecondary,
|
||||||
size: IconSize.input,
|
size: IconSize.input,
|
||||||
),
|
),
|
||||||
|
prefixIconConstraints: const BoxConstraints(
|
||||||
|
minWidth: TouchTarget.minimum,
|
||||||
|
minHeight: TouchTarget.minimum,
|
||||||
|
),
|
||||||
suffixIcon: _query.isNotEmpty
|
suffixIcon: _query.isNotEmpty
|
||||||
? IconButton(
|
? IconButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
@@ -218,6 +214,10 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
: null,
|
: null,
|
||||||
|
suffixIconConstraints: const BoxConstraints(
|
||||||
|
minWidth: TouchTarget.minimum,
|
||||||
|
minHeight: TouchTarget.minimum,
|
||||||
|
),
|
||||||
filled: true,
|
filled: true,
|
||||||
fillColor: theme.inputBackground,
|
fillColor: theme.inputBackground,
|
||||||
border: OutlineInputBorder(
|
border: OutlineInputBorder(
|
||||||
@@ -234,7 +234,7 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
|
|||||||
),
|
),
|
||||||
contentPadding: const EdgeInsets.symmetric(
|
contentPadding: const EdgeInsets.symmetric(
|
||||||
horizontal: Spacing.md,
|
horizontal: Spacing.md,
|
||||||
vertical: Spacing.md,
|
vertical: Spacing.xs,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -296,9 +296,15 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
|
|||||||
|
|
||||||
final children = <Widget>[
|
final children = <Widget>[
|
||||||
if (pinned.isNotEmpty) ...[
|
if (pinned.isNotEmpty) ...[
|
||||||
_buildSectionHeader(
|
Padding(
|
||||||
AppLocalizations.of(context)!.pinned,
|
padding: const EdgeInsets.only(
|
||||||
pinned.length,
|
left: Spacing.md,
|
||||||
|
right: Spacing.md,
|
||||||
|
),
|
||||||
|
child: _buildSectionHeader(
|
||||||
|
AppLocalizations.of(context)!.pinned,
|
||||||
|
pinned.length,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: Spacing.xs),
|
const SizedBox(height: Spacing.xs),
|
||||||
...pinned.map((conv) => _buildTileFor(conv)),
|
...pinned.map((conv) => _buildTileFor(conv)),
|
||||||
@@ -306,7 +312,13 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
|
|||||||
],
|
],
|
||||||
|
|
||||||
// Folders section (shown even if empty)
|
// Folders section (shown even if empty)
|
||||||
_buildFoldersSectionHeader(),
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(
|
||||||
|
left: Spacing.md,
|
||||||
|
right: Spacing.md,
|
||||||
|
),
|
||||||
|
child: _buildFoldersSectionHeader(),
|
||||||
|
),
|
||||||
const SizedBox(height: Spacing.xs),
|
const SizedBox(height: Spacing.xs),
|
||||||
if (_isDragging && _draggingHasFolder) ...[
|
if (_isDragging && _draggingHasFolder) ...[
|
||||||
_buildUnfileDropTarget(),
|
_buildUnfileDropTarget(),
|
||||||
@@ -356,9 +368,15 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
|
|||||||
const SizedBox(height: Spacing.md),
|
const SizedBox(height: Spacing.md),
|
||||||
|
|
||||||
if (regular.isNotEmpty) ...[
|
if (regular.isNotEmpty) ...[
|
||||||
_buildSectionHeader(
|
Padding(
|
||||||
AppLocalizations.of(context)!.recent,
|
padding: const EdgeInsets.only(
|
||||||
regular.length,
|
left: Spacing.md,
|
||||||
|
right: Spacing.md,
|
||||||
|
),
|
||||||
|
child: _buildSectionHeader(
|
||||||
|
AppLocalizations.of(context)!.recent,
|
||||||
|
regular.length,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: Spacing.xs),
|
const SizedBox(height: Spacing.xs),
|
||||||
...regular.map(_buildTileFor),
|
...regular.map(_buildTileFor),
|
||||||
@@ -366,7 +384,10 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
|
|||||||
|
|
||||||
if (archived.isNotEmpty) ...[
|
if (archived.isNotEmpty) ...[
|
||||||
const SizedBox(height: Spacing.md),
|
const SizedBox(height: Spacing.md),
|
||||||
_buildArchivedSection(archived),
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: Spacing.md),
|
||||||
|
child: _buildArchivedSection(archived),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
return _buildRefreshableScrollable(children: children);
|
return _buildRefreshableScrollable(children: children);
|
||||||
@@ -434,19 +455,28 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
|
|||||||
final archived = list.where((c) => c.archived == true).toList();
|
final archived = list.where((c) => c.archived == true).toList();
|
||||||
|
|
||||||
final children = <Widget>[
|
final children = <Widget>[
|
||||||
_buildSectionHeader('Results', list.length),
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(left: Spacing.md, right: Spacing.md),
|
||||||
|
child: _buildSectionHeader('Results', list.length),
|
||||||
|
),
|
||||||
const SizedBox(height: Spacing.xs),
|
const SizedBox(height: Spacing.xs),
|
||||||
if (pinned.isNotEmpty) ...[
|
if (pinned.isNotEmpty) ...[
|
||||||
_buildSectionHeader(
|
Padding(
|
||||||
AppLocalizations.of(context)!.pinned,
|
padding: const EdgeInsets.symmetric(horizontal: Spacing.md),
|
||||||
pinned.length,
|
child: _buildSectionHeader(
|
||||||
|
AppLocalizations.of(context)!.pinned,
|
||||||
|
pinned.length,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: Spacing.xs),
|
const SizedBox(height: Spacing.xs),
|
||||||
...pinned.map((conv) => _buildTileFor(conv)),
|
...pinned.map((conv) => _buildTileFor(conv)),
|
||||||
const SizedBox(height: Spacing.md),
|
const SizedBox(height: Spacing.md),
|
||||||
],
|
],
|
||||||
// Folders section (shown even if empty)
|
// Folders section (shown even if empty)
|
||||||
_buildFoldersSectionHeader(),
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(left: Spacing.md, right: Spacing.md),
|
||||||
|
child: _buildFoldersSectionHeader(),
|
||||||
|
),
|
||||||
const SizedBox(height: Spacing.xs),
|
const SizedBox(height: Spacing.xs),
|
||||||
if (_isDragging && _draggingHasFolder) ...[
|
if (_isDragging && _draggingHasFolder) ...[
|
||||||
_buildUnfileDropTarget(),
|
_buildUnfileDropTarget(),
|
||||||
@@ -491,16 +521,22 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: Spacing.md),
|
const SizedBox(height: Spacing.md),
|
||||||
if (regular.isNotEmpty) ...[
|
if (regular.isNotEmpty) ...[
|
||||||
_buildSectionHeader(
|
Padding(
|
||||||
AppLocalizations.of(context)!.recent,
|
padding: const EdgeInsets.symmetric(horizontal: Spacing.md),
|
||||||
regular.length,
|
child: _buildSectionHeader(
|
||||||
|
AppLocalizations.of(context)!.recent,
|
||||||
|
regular.length,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: Spacing.xs),
|
const SizedBox(height: Spacing.xs),
|
||||||
...regular.map(_buildTileFor),
|
...regular.map(_buildTileFor),
|
||||||
],
|
],
|
||||||
if (archived.isNotEmpty) ...[
|
if (archived.isNotEmpty) ...[
|
||||||
const SizedBox(height: Spacing.md),
|
const SizedBox(height: Spacing.md),
|
||||||
_buildArchivedSection(archived),
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: Spacing.md),
|
||||||
|
child: _buildArchivedSection(archived),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
return _buildRefreshableScrollable(children: children);
|
return _buildRefreshableScrollable(children: children);
|
||||||
@@ -645,21 +681,31 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
builder: (context, candidateData, rejectedData) {
|
builder: (context, candidateData, rejectedData) {
|
||||||
|
final baseColor = theme.surfaceContainer;
|
||||||
|
final hoverColor = theme.buttonPrimary.withValues(alpha: 0.08);
|
||||||
|
final borderColor = isHover
|
||||||
|
? theme.buttonPrimary.withValues(alpha: 0.60)
|
||||||
|
: theme.surfaceContainerHighest.withValues(alpha: 0.40);
|
||||||
|
|
||||||
|
Color? overlayForStates(Set<WidgetState> states) {
|
||||||
|
if (states.contains(WidgetState.pressed)) {
|
||||||
|
return theme.buttonPrimary.withValues(alpha: Alpha.buttonPressed);
|
||||||
|
}
|
||||||
|
if (states.contains(WidgetState.hovered) ||
|
||||||
|
states.contains(WidgetState.focused)) {
|
||||||
|
return theme.buttonPrimary.withValues(alpha: Alpha.hover);
|
||||||
|
}
|
||||||
|
return Colors.transparent;
|
||||||
|
}
|
||||||
|
|
||||||
return Material(
|
return Material(
|
||||||
color: isHover
|
color: isHover ? hoverColor : baseColor,
|
||||||
? theme.buttonPrimary.withValues(alpha: 0.08)
|
|
||||||
: theme.surfaceContainer.withValues(alpha: 0.05),
|
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(AppBorderRadius.md),
|
borderRadius: BorderRadius.zero,
|
||||||
side: BorderSide(
|
side: BorderSide(color: borderColor, width: BorderWidth.thin),
|
||||||
color: isHover
|
|
||||||
? theme.buttonPrimary.withValues(alpha: 0.6)
|
|
||||||
: theme.dividerColor,
|
|
||||||
width: BorderWidth.regular,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
borderRadius: BorderRadius.circular(AppBorderRadius.md),
|
borderRadius: BorderRadius.zero,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
final current = {...ref.read(_expandedFoldersProvider)};
|
final current = {...ref.read(_expandedFoldersProvider)};
|
||||||
current[folderId] = !isExpanded;
|
current[folderId] = !isExpanded;
|
||||||
@@ -669,53 +715,75 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
|
|||||||
HapticFeedback.selectionClick();
|
HapticFeedback.selectionClick();
|
||||||
_showFolderContextMenu(context, folderId, name);
|
_showFolderContextMenu(context, folderId, name);
|
||||||
},
|
},
|
||||||
child: Padding(
|
overlayColor: WidgetStateProperty.resolveWith(overlayForStates),
|
||||||
padding: const EdgeInsets.symmetric(
|
child: ConstrainedBox(
|
||||||
horizontal: Spacing.md,
|
constraints: const BoxConstraints(
|
||||||
vertical: Spacing.sm,
|
minHeight: TouchTarget.listItem,
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Padding(
|
||||||
children: [
|
padding: const EdgeInsets.symmetric(
|
||||||
Icon(
|
horizontal: Spacing.md,
|
||||||
isExpanded
|
vertical: Spacing.xs,
|
||||||
? (Platform.isIOS
|
),
|
||||||
? CupertinoIcons.folder_open
|
child: LayoutBuilder(
|
||||||
: Icons.folder_open)
|
builder: (context, constraints) {
|
||||||
: (Platform.isIOS
|
final hasFiniteWidth = constraints.maxWidth.isFinite;
|
||||||
? CupertinoIcons.folder
|
final textFit = hasFiniteWidth
|
||||||
: Icons.folder),
|
? FlexFit.tight
|
||||||
color: theme.iconPrimary,
|
: FlexFit.loose;
|
||||||
size: IconSize.listItem,
|
|
||||||
),
|
return Row(
|
||||||
const SizedBox(width: Spacing.sm),
|
mainAxisSize: hasFiniteWidth
|
||||||
Expanded(
|
? MainAxisSize.max
|
||||||
child: Text(
|
: MainAxisSize.min,
|
||||||
name,
|
children: [
|
||||||
style: AppTypography.standard.copyWith(
|
Icon(
|
||||||
color: theme.textPrimary,
|
isExpanded
|
||||||
fontWeight: FontWeight.w600,
|
? (Platform.isIOS
|
||||||
),
|
? CupertinoIcons.folder_open
|
||||||
),
|
: Icons.folder_open)
|
||||||
),
|
: (Platform.isIOS
|
||||||
Text(
|
? CupertinoIcons.folder
|
||||||
'$count',
|
: Icons.folder),
|
||||||
style: AppTypography.bodySmallStyle.copyWith(
|
color: theme.iconPrimary,
|
||||||
color: theme.textSecondary,
|
size: IconSize.listItem,
|
||||||
),
|
),
|
||||||
),
|
const SizedBox(width: Spacing.sm),
|
||||||
const SizedBox(width: Spacing.xs),
|
Flexible(
|
||||||
Icon(
|
fit: textFit,
|
||||||
isExpanded
|
child: Text(
|
||||||
? (Platform.isIOS
|
name,
|
||||||
? CupertinoIcons.chevron_up
|
maxLines: 1,
|
||||||
: Icons.expand_less)
|
overflow: TextOverflow.ellipsis,
|
||||||
: (Platform.isIOS
|
style: AppTypography.standard.copyWith(
|
||||||
? CupertinoIcons.chevron_down
|
color: theme.textPrimary,
|
||||||
: Icons.expand_more),
|
fontWeight: FontWeight.w400,
|
||||||
color: theme.iconSecondary,
|
),
|
||||||
size: IconSize.listItem,
|
),
|
||||||
),
|
),
|
||||||
],
|
const SizedBox(width: Spacing.sm),
|
||||||
|
Text(
|
||||||
|
'$count',
|
||||||
|
style: AppTypography.standard.copyWith(
|
||||||
|
color: theme.textSecondary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: Spacing.xs),
|
||||||
|
Icon(
|
||||||
|
isExpanded
|
||||||
|
? (Platform.isIOS
|
||||||
|
? CupertinoIcons.chevron_up
|
||||||
|
: Icons.expand_less)
|
||||||
|
: (Platform.isIOS
|
||||||
|
? CupertinoIcons.chevron_down
|
||||||
|
: Icons.expand_more),
|
||||||
|
color: theme.iconSecondary,
|
||||||
|
size: IconSize.listItem,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -929,9 +997,11 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
|
|||||||
final bool isLoadingSelected =
|
final bool isLoadingSelected =
|
||||||
(_pendingConversationId == conv.id) &&
|
(_pendingConversationId == conv.id) &&
|
||||||
(ref.watch(chat.isLoadingConversationProvider) == true);
|
(ref.watch(chat.isLoadingConversationProvider) == true);
|
||||||
|
final bool isPinned = conv.pinned == true;
|
||||||
|
|
||||||
final tile = _ConversationTile(
|
final tile = _ConversationTile(
|
||||||
title: title,
|
title: title,
|
||||||
pinned: conv.pinned == true,
|
pinned: isPinned,
|
||||||
selected: isActive,
|
selected: isActive,
|
||||||
isLoading: isLoadingSelected,
|
isLoading: isLoadingSelected,
|
||||||
onTap: _isLoadingConversation
|
onTap: _isLoadingConversation
|
||||||
@@ -943,6 +1013,7 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
|
|||||||
_showConversationContextMenu(context, conv);
|
_showConversationContextMenu(context, conv);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: EdgeInsets.only(
|
padding: EdgeInsets.only(
|
||||||
bottom: Spacing.xs,
|
bottom: Spacing.xs,
|
||||||
@@ -951,41 +1022,10 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
|
|||||||
child: LongPressDraggable<_DragConversationData>(
|
child: LongPressDraggable<_DragConversationData>(
|
||||||
data: _DragConversationData(id: conv.id, title: title),
|
data: _DragConversationData(id: conv.id, title: title),
|
||||||
dragAnchorStrategy: pointerDragAnchorStrategy,
|
dragAnchorStrategy: pointerDragAnchorStrategy,
|
||||||
feedback: Material(
|
feedback: _ConversationDragFeedback(
|
||||||
color: Colors.transparent,
|
title: title,
|
||||||
elevation: 6,
|
pinned: isPinned,
|
||||||
borderRadius: BorderRadius.circular(AppBorderRadius.md),
|
theme: theme,
|
||||||
child: Opacity(
|
|
||||||
opacity: 0.9,
|
|
||||||
child: Container(
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: Spacing.md,
|
|
||||||
vertical: Spacing.sm,
|
|
||||||
),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: theme.cardBackground,
|
|
||||||
borderRadius: BorderRadius.circular(AppBorderRadius.md),
|
|
||||||
border: Border.all(
|
|
||||||
color: theme.dividerColor,
|
|
||||||
width: BorderWidth.regular,
|
|
||||||
),
|
|
||||||
boxShadow: ConduitShadows.card,
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
Icon(
|
|
||||||
Platform.isIOS
|
|
||||||
? CupertinoIcons.chat_bubble_2
|
|
||||||
: Icons.chat_bubble_outline,
|
|
||||||
size: IconSize.listItem,
|
|
||||||
),
|
|
||||||
const SizedBox(width: Spacing.xs),
|
|
||||||
Text(title, maxLines: 1, overflow: TextOverflow.ellipsis),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
childWhenDragging: Opacity(
|
childWhenDragging: Opacity(
|
||||||
opacity: 0.5,
|
opacity: 0.5,
|
||||||
@@ -1017,60 +1057,97 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
Material(
|
Material(
|
||||||
color: theme.surfaceContainer.withValues(alpha: 0.05),
|
color: show
|
||||||
|
? theme.navigationSelectedBackground
|
||||||
|
: theme.surfaceContainer,
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(AppBorderRadius.md),
|
borderRadius: BorderRadius.zero,
|
||||||
side: BorderSide(
|
side: BorderSide(
|
||||||
color: theme.dividerColor,
|
color: show
|
||||||
width: BorderWidth.regular,
|
? theme.navigationSelected
|
||||||
|
: theme.surfaceContainerHighest.withValues(alpha: 0.40),
|
||||||
|
width: BorderWidth.thin,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
borderRadius: BorderRadius.circular(AppBorderRadius.md),
|
borderRadius: BorderRadius.zero,
|
||||||
onTap: () => ref.read(_showArchivedProvider.notifier).state = !show,
|
onTap: () => ref.read(_showArchivedProvider.notifier).state = !show,
|
||||||
child: Padding(
|
overlayColor: WidgetStateProperty.resolveWith((states) {
|
||||||
padding: const EdgeInsets.symmetric(
|
if (states.contains(WidgetState.pressed)) {
|
||||||
horizontal: Spacing.md,
|
return theme.buttonPrimary.withValues(
|
||||||
vertical: Spacing.sm,
|
alpha: Alpha.buttonPressed,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (states.contains(WidgetState.hovered) ||
|
||||||
|
states.contains(WidgetState.focused)) {
|
||||||
|
return theme.buttonPrimary.withValues(alpha: Alpha.hover);
|
||||||
|
}
|
||||||
|
return Colors.transparent;
|
||||||
|
}),
|
||||||
|
child: ConstrainedBox(
|
||||||
|
constraints: const BoxConstraints(
|
||||||
|
minHeight: TouchTarget.listItem,
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Padding(
|
||||||
children: [
|
padding: const EdgeInsets.symmetric(
|
||||||
Icon(
|
horizontal: Spacing.md,
|
||||||
Platform.isIOS
|
vertical: Spacing.xs,
|
||||||
? CupertinoIcons.archivebox
|
),
|
||||||
: Icons.archive_rounded,
|
child: LayoutBuilder(
|
||||||
color: theme.iconPrimary,
|
builder: (context, constraints) {
|
||||||
size: IconSize.listItem,
|
final hasFiniteWidth = constraints.maxWidth.isFinite;
|
||||||
),
|
final textFit = hasFiniteWidth
|
||||||
const SizedBox(width: Spacing.sm),
|
? FlexFit.tight
|
||||||
Expanded(
|
: FlexFit.loose;
|
||||||
child: Text(
|
|
||||||
AppLocalizations.of(context)!.archived,
|
return Row(
|
||||||
style: AppTypography.bodyLargeStyle.copyWith(
|
mainAxisSize: hasFiniteWidth
|
||||||
color: theme.textPrimary,
|
? MainAxisSize.max
|
||||||
fontWeight: FontWeight.w600,
|
: MainAxisSize.min,
|
||||||
),
|
children: [
|
||||||
),
|
Icon(
|
||||||
),
|
Platform.isIOS
|
||||||
Text(
|
? CupertinoIcons.archivebox
|
||||||
'${archived.length}',
|
: Icons.archive_rounded,
|
||||||
style: AppTypography.bodySmallStyle.copyWith(
|
color: theme.iconPrimary,
|
||||||
color: theme.textSecondary,
|
size: IconSize.listItem,
|
||||||
),
|
),
|
||||||
),
|
const SizedBox(width: Spacing.sm),
|
||||||
const SizedBox(width: Spacing.xs),
|
Flexible(
|
||||||
Icon(
|
fit: textFit,
|
||||||
show
|
child: Text(
|
||||||
? (Platform.isIOS
|
AppLocalizations.of(context)!.archived,
|
||||||
? CupertinoIcons.chevron_up
|
maxLines: 1,
|
||||||
: Icons.expand_less)
|
overflow: TextOverflow.ellipsis,
|
||||||
: (Platform.isIOS
|
style: AppTypography.standard.copyWith(
|
||||||
? CupertinoIcons.chevron_down
|
color: theme.textPrimary,
|
||||||
: Icons.expand_more),
|
fontWeight: FontWeight.w400,
|
||||||
color: theme.iconSecondary,
|
),
|
||||||
size: IconSize.listItem,
|
),
|
||||||
),
|
),
|
||||||
],
|
const SizedBox(width: Spacing.sm),
|
||||||
|
Text(
|
||||||
|
'${archived.length}',
|
||||||
|
style: AppTypography.standard.copyWith(
|
||||||
|
color: theme.textSecondary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: Spacing.xs),
|
||||||
|
Icon(
|
||||||
|
show
|
||||||
|
? (Platform.isIOS
|
||||||
|
? CupertinoIcons.chevron_up
|
||||||
|
: Icons.expand_less)
|
||||||
|
: (Platform.isIOS
|
||||||
|
? CupertinoIcons.chevron_down
|
||||||
|
: Icons.expand_more),
|
||||||
|
color: theme.iconSecondary,
|
||||||
|
size: IconSize.listItem,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -1212,14 +1289,21 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
TextButton(
|
IconButton(
|
||||||
|
tooltip: AppLocalizations.of(context)!.manage,
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Navigator.of(context).maybePop();
|
Navigator.of(context).maybePop();
|
||||||
Navigator.of(context).push(
|
Navigator.of(context).push(
|
||||||
MaterialPageRoute(builder: (_) => const ProfilePage()),
|
MaterialPageRoute(builder: (_) => const ProfilePage()),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
child: Text(AppLocalizations.of(context)!.manage),
|
icon: Icon(
|
||||||
|
Platform.isIOS
|
||||||
|
? CupertinoIcons.settings
|
||||||
|
: Icons.settings_rounded,
|
||||||
|
color: theme.iconSecondary,
|
||||||
|
size: IconSize.listItem,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -1436,6 +1520,148 @@ class _DragConversationData {
|
|||||||
const _DragConversationData({required this.id, required this.title});
|
const _DragConversationData({required this.id, required this.title});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _ConversationDragFeedback extends StatelessWidget {
|
||||||
|
final String title;
|
||||||
|
final bool pinned;
|
||||||
|
final ConduitThemeExtension theme;
|
||||||
|
|
||||||
|
const _ConversationDragFeedback({
|
||||||
|
required this.title,
|
||||||
|
required this.pinned,
|
||||||
|
required this.theme,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final borderColor = pinned
|
||||||
|
? theme.navigationSelected.withValues(alpha: 0.35)
|
||||||
|
: theme.surfaceContainerHighest.withValues(alpha: 0.45);
|
||||||
|
|
||||||
|
return Material(
|
||||||
|
color: Colors.transparent,
|
||||||
|
elevation: Elevation.low,
|
||||||
|
borderRadius: BorderRadius.zero,
|
||||||
|
child: Container(
|
||||||
|
constraints: const BoxConstraints(minHeight: TouchTarget.listItem),
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: Spacing.md,
|
||||||
|
vertical: Spacing.xs,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: theme.surfaceContainer,
|
||||||
|
borderRadius: BorderRadius.zero,
|
||||||
|
border: Border.all(color: borderColor, width: BorderWidth.thin),
|
||||||
|
),
|
||||||
|
child: _ConversationTileContent(
|
||||||
|
title: title,
|
||||||
|
pinned: pinned,
|
||||||
|
selected: false,
|
||||||
|
isLoading: false,
|
||||||
|
onMorePressed: null,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ConversationTileContent extends StatelessWidget {
|
||||||
|
final String title;
|
||||||
|
final bool pinned;
|
||||||
|
final bool selected;
|
||||||
|
final bool isLoading;
|
||||||
|
final VoidCallback? onMorePressed;
|
||||||
|
|
||||||
|
const _ConversationTileContent({
|
||||||
|
required this.title,
|
||||||
|
required this.pinned,
|
||||||
|
required this.selected,
|
||||||
|
required this.isLoading,
|
||||||
|
this.onMorePressed,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = context.conduitTheme;
|
||||||
|
final textStyle = AppTypography.standard.copyWith(
|
||||||
|
color: theme.textPrimary,
|
||||||
|
fontWeight: FontWeight.w400,
|
||||||
|
height: 1.4,
|
||||||
|
);
|
||||||
|
|
||||||
|
return LayoutBuilder(
|
||||||
|
builder: (context, constraints) {
|
||||||
|
final hasFiniteWidth = constraints.maxWidth.isFinite;
|
||||||
|
final textFit = hasFiniteWidth ? FlexFit.tight : FlexFit.loose;
|
||||||
|
|
||||||
|
final trailing = <Widget>[];
|
||||||
|
if (pinned) {
|
||||||
|
trailing.addAll([
|
||||||
|
const SizedBox(width: Spacing.xs),
|
||||||
|
Icon(
|
||||||
|
Platform.isIOS ? CupertinoIcons.pin_fill : Icons.push_pin_rounded,
|
||||||
|
color: theme.iconSecondary,
|
||||||
|
size: IconSize.xs,
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
trailing.addAll([
|
||||||
|
const SizedBox(width: Spacing.sm),
|
||||||
|
SizedBox(
|
||||||
|
width: IconSize.sm,
|
||||||
|
height: IconSize.sm,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
strokeWidth: BorderWidth.medium,
|
||||||
|
valueColor: AlwaysStoppedAnimation<Color>(
|
||||||
|
theme.loadingIndicator,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
} else if (onMorePressed != null) {
|
||||||
|
trailing.addAll([
|
||||||
|
const SizedBox(width: Spacing.sm),
|
||||||
|
IconButton(
|
||||||
|
iconSize: IconSize.sm,
|
||||||
|
visualDensity: const VisualDensity(horizontal: -2, vertical: -2),
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
constraints: const BoxConstraints(
|
||||||
|
minWidth: TouchTarget.listItem,
|
||||||
|
minHeight: TouchTarget.listItem,
|
||||||
|
),
|
||||||
|
icon: Icon(
|
||||||
|
Platform.isIOS
|
||||||
|
? CupertinoIcons.ellipsis
|
||||||
|
: Icons.more_vert_rounded,
|
||||||
|
color: theme.iconSecondary,
|
||||||
|
),
|
||||||
|
onPressed: onMorePressed,
|
||||||
|
tooltip: AppLocalizations.of(context)!.more,
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Row(
|
||||||
|
mainAxisSize: hasFiniteWidth ? MainAxisSize.max : MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Flexible(
|
||||||
|
fit: textFit,
|
||||||
|
child: Text(
|
||||||
|
title,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: textStyle,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
...trailing,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class _ConversationTile extends StatelessWidget {
|
class _ConversationTile extends StatelessWidget {
|
||||||
final String title;
|
final String title;
|
||||||
final bool pinned;
|
final bool pinned;
|
||||||
@@ -1458,102 +1684,66 @@ class _ConversationTile extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = context.conduitTheme;
|
final theme = context.conduitTheme;
|
||||||
final Color selectedBackground = theme.buttonPrimary.withValues(
|
final borderColor = selected
|
||||||
alpha: 0.10,
|
? theme.navigationSelected
|
||||||
); // subtle highlight
|
: pinned
|
||||||
final Color selectedBorder = theme.buttonPrimary.withValues(alpha: 0.60);
|
? theme.navigationSelected.withValues(alpha: 0.30)
|
||||||
|
: theme.surfaceContainerHighest.withValues(alpha: 0.40);
|
||||||
|
final backgroundColor = theme.surfaceContainer;
|
||||||
|
final highlightColor = theme.navigationSelectedBackground.withValues(
|
||||||
|
alpha: 0.45,
|
||||||
|
);
|
||||||
|
|
||||||
|
Color? overlayForStates(Set<WidgetState> states) {
|
||||||
|
if (states.contains(WidgetState.pressed)) {
|
||||||
|
return theme.buttonPrimary.withValues(alpha: Alpha.buttonPressed);
|
||||||
|
}
|
||||||
|
if (states.contains(WidgetState.focused) ||
|
||||||
|
states.contains(WidgetState.hovered)) {
|
||||||
|
return theme.buttonPrimary.withValues(alpha: Alpha.hover);
|
||||||
|
}
|
||||||
|
return Colors.transparent;
|
||||||
|
}
|
||||||
|
|
||||||
return Semantics(
|
return Semantics(
|
||||||
selected: selected,
|
selected: selected,
|
||||||
button: true,
|
button: true,
|
||||||
child: Material(
|
child: Material(
|
||||||
color: selected ? selectedBackground : Colors.transparent,
|
color: backgroundColor,
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(AppBorderRadius.md),
|
borderRadius: BorderRadius.zero,
|
||||||
side: BorderSide(
|
side: BorderSide(color: borderColor, width: BorderWidth.thin),
|
||||||
color: selected ? selectedBorder : theme.dividerColor,
|
|
||||||
width: BorderWidth.regular,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
borderRadius: BorderRadius.circular(AppBorderRadius.md),
|
borderRadius: BorderRadius.zero,
|
||||||
onTap: isLoading ? null : onTap,
|
onTap: isLoading ? null : onTap,
|
||||||
onLongPress: onLongPress,
|
onLongPress: onLongPress,
|
||||||
child: Stack(
|
overlayColor: WidgetStateProperty.resolveWith(overlayForStates),
|
||||||
children: [
|
child: AnimatedContainer(
|
||||||
// Left accent bar for active conversation
|
duration: const Duration(milliseconds: 160),
|
||||||
AnimatedPositioned(
|
curve: Curves.easeOut,
|
||||||
duration: const Duration(milliseconds: 160),
|
decoration: BoxDecoration(
|
||||||
curve: Curves.easeOut,
|
color: selected ? highlightColor : Colors.transparent,
|
||||||
left: 0,
|
borderRadius: BorderRadius.zero,
|
||||||
top: 0,
|
),
|
||||||
bottom: 0,
|
child: ConstrainedBox(
|
||||||
child: AnimatedContainer(
|
constraints: const BoxConstraints(
|
||||||
duration: const Duration(milliseconds: 160),
|
minHeight: TouchTarget.listItem,
|
||||||
width: selected ? 3.0 : 0.0,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: theme.buttonPrimary,
|
|
||||||
borderRadius: const BorderRadius.only(
|
|
||||||
topLeft: Radius.circular(AppBorderRadius.md),
|
|
||||||
bottomLeft: Radius.circular(AppBorderRadius.md),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
horizontal: Spacing.md,
|
horizontal: Spacing.md,
|
||||||
vertical: Spacing.sm,
|
vertical: Spacing.xs,
|
||||||
),
|
),
|
||||||
child: Row(
|
child: _ConversationTileContent(
|
||||||
children: [
|
title: title,
|
||||||
Expanded(
|
pinned: pinned,
|
||||||
child: Text(
|
selected: selected,
|
||||||
title,
|
isLoading: isLoading,
|
||||||
maxLines: 1,
|
onMorePressed: onMorePressed,
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
style: AppTypography.standard.copyWith(
|
|
||||||
color: theme.textPrimary,
|
|
||||||
fontWeight: selected
|
|
||||||
? FontWeight.w700
|
|
||||||
: FontWeight.w500,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: Spacing.xs),
|
|
||||||
if (isLoading)
|
|
||||||
SizedBox(
|
|
||||||
width: IconSize.sm,
|
|
||||||
height: IconSize.sm,
|
|
||||||
child: CircularProgressIndicator(
|
|
||||||
strokeWidth: BorderWidth.medium,
|
|
||||||
valueColor: AlwaysStoppedAnimation<Color>(
|
|
||||||
theme.loadingIndicator,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
else if (onMorePressed != null)
|
|
||||||
IconButton(
|
|
||||||
visualDensity: VisualDensity.compact,
|
|
||||||
padding: EdgeInsets.zero,
|
|
||||||
constraints: const BoxConstraints(
|
|
||||||
minWidth: TouchTarget.listItem,
|
|
||||||
minHeight: TouchTarget.listItem,
|
|
||||||
),
|
|
||||||
icon: Icon(
|
|
||||||
Platform.isIOS
|
|
||||||
? CupertinoIcons.ellipsis
|
|
||||||
: Icons.more_vert_rounded,
|
|
||||||
color: theme.iconSecondary,
|
|
||||||
size: IconSize.listItem,
|
|
||||||
),
|
|
||||||
onPressed: onMorePressed,
|
|
||||||
tooltip: AppLocalizations.of(context)!.more,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
Reference in New Issue
Block a user