feat: enhance localization support with additional strings and improved structure
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:conduit/l10n/app_localizations.dart';
|
||||
import '../../../core/widgets/error_boundary.dart';
|
||||
import '../../../shared/widgets/optimized_list.dart';
|
||||
import '../../../shared/theme/theme_extensions.dart';
|
||||
@@ -805,7 +806,7 @@ class _ChatPageState extends ConsumerState<ChatPage> {
|
||||
style: TextStyle(color: context.conduitTheme.textPrimary),
|
||||
maxLines: null,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Enter your message',
|
||||
hintText: AppLocalizations.of(context)!.messageHintText,
|
||||
hintStyle: TextStyle(color: context.conduitTheme.inputPlaceholder),
|
||||
border: OutlineInputBorder(
|
||||
borderSide: BorderSide(color: context.conduitTheme.inputBorder),
|
||||
@@ -831,7 +832,7 @@ class _ChatPageState extends ConsumerState<ChatPage> {
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: context.conduitTheme.buttonPrimary,
|
||||
),
|
||||
child: const Text('Save'),
|
||||
child: Text(AppLocalizations.of(context)!.save),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -911,7 +912,7 @@ class _ChatPageState extends ConsumerState<ChatPage> {
|
||||
const SizedBox(height: Spacing.xl),
|
||||
|
||||
Text(
|
||||
'Start a conversation',
|
||||
AppLocalizations.of(context)!.onboardStartTitle,
|
||||
style: theme.textTheme.headlineSmall?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: context.conduitTheme.textPrimary,
|
||||
@@ -921,7 +922,7 @@ class _ChatPageState extends ConsumerState<ChatPage> {
|
||||
const SizedBox(height: Spacing.sm),
|
||||
|
||||
Text(
|
||||
'Type below to begin',
|
||||
AppLocalizations.of(context)!.typeBelowToBegin,
|
||||
style: theme.textTheme.bodyLarge?.copyWith(
|
||||
color: context.conduitTheme.textSecondary,
|
||||
fontWeight: FontWeight.w400,
|
||||
@@ -1221,7 +1222,7 @@ class _ChatPageState extends ConsumerState<ChatPage> {
|
||||
size: IconSize.appBar,
|
||||
),
|
||||
onPressed: _handleNewChat,
|
||||
tooltip: 'New Chat',
|
||||
tooltip: AppLocalizations.of(context)!.newChat,
|
||||
),
|
||||
] else ...[
|
||||
IconButton(
|
||||
@@ -1530,7 +1531,7 @@ class _ModelSelectorSheetState extends ConsumerState<_ModelSelectorSheet> {
|
||||
controller: _searchController,
|
||||
style: TextStyle(color: context.conduitTheme.textPrimary),
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Search...',
|
||||
hintText: AppLocalizations.of(context)!.searchModels,
|
||||
hintStyle: TextStyle(
|
||||
color: context.conduitTheme.inputPlaceholder,
|
||||
),
|
||||
@@ -2235,7 +2236,9 @@ class _VoiceInputSheetState extends ConsumerState<_VoiceInputSheet> {
|
||||
icon: Platform.isIOS
|
||||
? CupertinoIcons.xmark
|
||||
: Icons.close,
|
||||
tooltip: 'Close',
|
||||
tooltip: AppLocalizations.of(
|
||||
context,
|
||||
)!.closeButtonSemantic,
|
||||
isCompact: true,
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
),
|
||||
@@ -2475,7 +2478,9 @@ class _VoiceInputSheetState extends ConsumerState<_VoiceInputSheet> {
|
||||
ConduitIconButton(
|
||||
icon: Icons.close,
|
||||
isCompact: true,
|
||||
tooltip: 'Clear',
|
||||
tooltip: AppLocalizations.of(
|
||||
context,
|
||||
)!.clear,
|
||||
onPressed:
|
||||
_recognizedText.isNotEmpty &&
|
||||
!_isTranscribing
|
||||
|
||||
@@ -393,7 +393,9 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
|
||||
icon: Platform.isIOS
|
||||
? CupertinoIcons.search
|
||||
: Icons.search,
|
||||
label: 'Web',
|
||||
label: AppLocalizations.of(
|
||||
context,
|
||||
)!.web,
|
||||
isActive: webSearchEnabled,
|
||||
onTap: widget.enabled
|
||||
? () {
|
||||
@@ -413,7 +415,9 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
|
||||
icon: Platform.isIOS
|
||||
? CupertinoIcons.photo
|
||||
: Icons.image,
|
||||
label: 'Image Gen',
|
||||
label: AppLocalizations.of(
|
||||
context,
|
||||
)!.imageGen,
|
||||
isActive: imageGenEnabled,
|
||||
onTap: widget.enabled
|
||||
? () {
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'dart:io' show File, Platform;
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:conduit/l10n/app_localizations.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../../../core/providers/app_providers.dart';
|
||||
@@ -353,12 +354,12 @@ class _VoiceInputSheetState extends ConsumerState<VoiceInputSheet> {
|
||||
children: [
|
||||
Text(
|
||||
_isTranscribing
|
||||
? 'Transcribing…'
|
||||
? AppLocalizations.of(context)!.transcribing
|
||||
: _isListening
|
||||
? (_voiceService.hasLocalStt
|
||||
? 'Listening…'
|
||||
: 'Recording…')
|
||||
: 'Voice',
|
||||
? AppLocalizations.of(context)!.listening
|
||||
: AppLocalizations.of(context)!.recording)
|
||||
: AppLocalizations.of(context)!.voiceInput,
|
||||
style: TextStyle(
|
||||
fontSize: AppTypography.headlineMedium,
|
||||
fontWeight: FontWeight.w600,
|
||||
@@ -426,7 +427,9 @@ class _VoiceInputSheetState extends ConsumerState<VoiceInputSheet> {
|
||||
icon: Platform.isIOS
|
||||
? CupertinoIcons.xmark
|
||||
: Icons.close,
|
||||
tooltip: 'Close',
|
||||
tooltip: AppLocalizations.of(
|
||||
context,
|
||||
)!.closeButtonSemantic,
|
||||
isCompact: true,
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
),
|
||||
@@ -456,11 +459,16 @@ class _VoiceInputSheetState extends ConsumerState<VoiceInputSheet> {
|
||||
activeColor: context.conduitTheme.buttonPrimary,
|
||||
),
|
||||
const SizedBox(width: Spacing.xs),
|
||||
Text(
|
||||
'Hold to talk',
|
||||
style: TextStyle(color: context.conduitTheme.textSecondary),
|
||||
Flexible(
|
||||
child: Text(
|
||||
AppLocalizations.of(context)!.holdToTalk,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: context.conduitTheme.textSecondary,
|
||||
),
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
const SizedBox(width: Spacing.sm),
|
||||
ps.PlatformService.getPlatformSwitch(
|
||||
value: _autoSendFinal,
|
||||
onChanged: (v) async {
|
||||
@@ -472,9 +480,14 @@ class _VoiceInputSheetState extends ConsumerState<VoiceInputSheet> {
|
||||
activeColor: context.conduitTheme.buttonPrimary,
|
||||
),
|
||||
const SizedBox(width: Spacing.xs),
|
||||
Text(
|
||||
'Auto-send',
|
||||
style: TextStyle(color: context.conduitTheme.textSecondary),
|
||||
Flexible(
|
||||
child: Text(
|
||||
AppLocalizations.of(context)!.autoSend,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: context.conduitTheme.textSecondary,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -521,8 +534,10 @@ class _VoiceInputSheetState extends ConsumerState<VoiceInputSheet> {
|
||||
child: Semantics(
|
||||
button: true,
|
||||
label: _isListening
|
||||
? 'Stop listening'
|
||||
: 'Start listening',
|
||||
? AppLocalizations.of(context)!.stopListening
|
||||
: AppLocalizations.of(
|
||||
context,
|
||||
)!.startListening,
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
@@ -617,7 +632,9 @@ class _VoiceInputSheetState extends ConsumerState<VoiceInputSheet> {
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
'Transcript',
|
||||
AppLocalizations.of(
|
||||
context,
|
||||
)!.transcript,
|
||||
style: TextStyle(
|
||||
fontSize: AppTypography.labelSmall,
|
||||
fontWeight: FontWeight.w600,
|
||||
@@ -630,7 +647,9 @@ class _VoiceInputSheetState extends ConsumerState<VoiceInputSheet> {
|
||||
ConduitIconButton(
|
||||
icon: Icons.close,
|
||||
isCompact: true,
|
||||
tooltip: 'Clear',
|
||||
tooltip: AppLocalizations.of(
|
||||
context,
|
||||
)!.clear,
|
||||
onPressed:
|
||||
_recognizedText.isNotEmpty &&
|
||||
!_isTranscribing
|
||||
@@ -656,18 +675,13 @@ class _VoiceInputSheetState extends ConsumerState<VoiceInputSheet> {
|
||||
),
|
||||
const SizedBox(width: Spacing.xs),
|
||||
Text(
|
||||
'Transcribing…',
|
||||
AppLocalizations.of(
|
||||
context,
|
||||
)!.transcribing,
|
||||
style: TextStyle(
|
||||
fontSize: isUltra
|
||||
? AppTypography.bodySmall
|
||||
: (isCompact
|
||||
? AppTypography
|
||||
.bodyMedium
|
||||
: AppTypography
|
||||
.bodyLarge),
|
||||
color: context
|
||||
.conduitTheme
|
||||
.textSecondary,
|
||||
? 12
|
||||
: (isCompact ? 12 : 13),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -680,9 +694,15 @@ class _VoiceInputSheetState extends ConsumerState<VoiceInputSheet> {
|
||||
_recognizedText.isEmpty
|
||||
? (_isListening
|
||||
? (_voiceService.hasLocalStt
|
||||
? 'Speak now…'
|
||||
: 'Recording…')
|
||||
: 'Tap Start to begin')
|
||||
? AppLocalizations.of(
|
||||
context,
|
||||
)!.speakNow
|
||||
: AppLocalizations.of(
|
||||
context,
|
||||
)!.recording)
|
||||
: AppLocalizations.of(
|
||||
context,
|
||||
)!.typeBelowToBegin)
|
||||
: _recognizedText,
|
||||
style: TextStyle(
|
||||
fontSize: isUltra
|
||||
@@ -731,7 +751,9 @@ class _VoiceInputSheetState extends ConsumerState<VoiceInputSheet> {
|
||||
children: [
|
||||
Expanded(
|
||||
child: ConduitButton(
|
||||
text: _isListening ? 'Stop' : 'Start',
|
||||
text: _isListening
|
||||
? AppLocalizations.of(context)!.stop
|
||||
: AppLocalizations.of(context)!.start,
|
||||
isSecondary: true,
|
||||
isCompact: isCompact,
|
||||
onPressed: _isListening
|
||||
@@ -742,7 +764,7 @@ class _VoiceInputSheetState extends ConsumerState<VoiceInputSheet> {
|
||||
const SizedBox(width: Spacing.xs),
|
||||
Expanded(
|
||||
child: ConduitButton(
|
||||
text: 'Send',
|
||||
text: AppLocalizations.of(context)!.send,
|
||||
isCompact: isCompact,
|
||||
onPressed: _recognizedText.isNotEmpty ? _sendText : null,
|
||||
),
|
||||
|
||||
@@ -34,8 +34,9 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
|
||||
|
||||
// UI state providers for sections
|
||||
static final _showArchivedProvider = StateProvider<bool>((ref) => false);
|
||||
static final _expandedFoldersProvider =
|
||||
StateProvider<Map<String, bool>>((ref) => {});
|
||||
static final _expandedFoldersProvider = StateProvider<Map<String, bool>>(
|
||||
(ref) => {},
|
||||
);
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
@@ -96,7 +97,7 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
|
||||
children: [
|
||||
// Centered title (no leading icon)
|
||||
Text(
|
||||
'Chats',
|
||||
AppLocalizations.of(context)!.chats,
|
||||
style: AppTypography.headlineSmallStyle.copyWith(
|
||||
color: theme.textPrimary,
|
||||
fontWeight: FontWeight.w600,
|
||||
@@ -214,17 +215,21 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
|
||||
// Build sections
|
||||
final pinned = list.where((c) => c.pinned == true).toList();
|
||||
final regular = list
|
||||
.where((c) =>
|
||||
c.pinned != true &&
|
||||
c.archived != true &&
|
||||
(c.folderId == null || c.folderId!.isEmpty))
|
||||
.where(
|
||||
(c) =>
|
||||
c.pinned != true &&
|
||||
c.archived != true &&
|
||||
(c.folderId == null || c.folderId!.isEmpty),
|
||||
)
|
||||
.toList();
|
||||
final foldered = list
|
||||
.where((c) =>
|
||||
c.pinned != true &&
|
||||
c.archived != true &&
|
||||
c.folderId != null &&
|
||||
c.folderId!.isNotEmpty)
|
||||
.where(
|
||||
(c) =>
|
||||
c.pinned != true &&
|
||||
c.archived != true &&
|
||||
c.folderId != null &&
|
||||
c.folderId!.isNotEmpty,
|
||||
)
|
||||
.toList();
|
||||
final archived = list.where((c) => c.archived == true).toList();
|
||||
|
||||
@@ -237,7 +242,10 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
|
||||
),
|
||||
children: [
|
||||
if (pinned.isNotEmpty) ...[
|
||||
_buildSectionHeader('Pinned', pinned.length),
|
||||
_buildSectionHeader(
|
||||
AppLocalizations.of(context)!.pinned,
|
||||
pinned.length,
|
||||
),
|
||||
const SizedBox(height: Spacing.xs),
|
||||
...pinned.map((conv) => _buildTileFor(conv)),
|
||||
const SizedBox(height: Spacing.md),
|
||||
@@ -250,40 +258,53 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
|
||||
_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);
|
||||
}
|
||||
...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),
|
||||
],
|
||||
// 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()],
|
||||
),
|
||||
],
|
||||
);
|
||||
}).toList();
|
||||
return sections.isEmpty
|
||||
? [const SizedBox.shrink()]
|
||||
: sections;
|
||||
},
|
||||
loading: () => [const SizedBox.shrink()],
|
||||
error: (e, st) => [const SizedBox.shrink()],
|
||||
),
|
||||
const SizedBox(height: Spacing.md),
|
||||
|
||||
if (regular.isNotEmpty) ...[
|
||||
_buildSectionHeader(AppLocalizations.of(context)!.recent, regular.length),
|
||||
_buildSectionHeader(
|
||||
AppLocalizations.of(context)!.recent,
|
||||
regular.length,
|
||||
),
|
||||
const SizedBox(height: Spacing.xs),
|
||||
...regular.map(_buildTileFor),
|
||||
],
|
||||
@@ -295,7 +316,8 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
|
||||
],
|
||||
);
|
||||
},
|
||||
loading: () => const Center(child: CircularProgressIndicator(strokeWidth: 2.0)),
|
||||
loading: () =>
|
||||
const Center(child: CircularProgressIndicator(strokeWidth: 2.0)),
|
||||
error: (e, _) => Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(Spacing.md),
|
||||
@@ -330,17 +352,21 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
|
||||
|
||||
final pinned = list.where((c) => c.pinned == true).toList();
|
||||
final regular = list
|
||||
.where((c) =>
|
||||
c.pinned != true &&
|
||||
c.archived != true &&
|
||||
(c.folderId == null || c.folderId!.isEmpty))
|
||||
.where(
|
||||
(c) =>
|
||||
c.pinned != true &&
|
||||
c.archived != true &&
|
||||
(c.folderId == null || c.folderId!.isEmpty),
|
||||
)
|
||||
.toList();
|
||||
final foldered = list
|
||||
.where((c) =>
|
||||
c.pinned != true &&
|
||||
c.archived != true &&
|
||||
c.folderId != null &&
|
||||
c.folderId!.isNotEmpty)
|
||||
.where(
|
||||
(c) =>
|
||||
c.pinned != true &&
|
||||
c.archived != true &&
|
||||
c.folderId != null &&
|
||||
c.folderId!.isNotEmpty,
|
||||
)
|
||||
.toList();
|
||||
final archived = list.where((c) => c.archived == true).toList();
|
||||
|
||||
@@ -355,7 +381,10 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
|
||||
_buildSectionHeader('Results', list.length),
|
||||
const SizedBox(height: Spacing.xs),
|
||||
if (pinned.isNotEmpty) ...[
|
||||
_buildSectionHeader('Pinned', pinned.length),
|
||||
_buildSectionHeader(
|
||||
AppLocalizations.of(context)!.pinned,
|
||||
pinned.length,
|
||||
),
|
||||
const SizedBox(height: Spacing.xs),
|
||||
...pinned.map((conv) => _buildTileFor(conv)),
|
||||
const SizedBox(height: Spacing.md),
|
||||
@@ -367,7 +396,9 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
|
||||
_buildUnfileDropTarget(),
|
||||
const SizedBox(height: Spacing.sm),
|
||||
],
|
||||
...ref.watch(foldersProvider).when(
|
||||
...ref
|
||||
.watch(foldersProvider)
|
||||
.when(
|
||||
data: (folders) {
|
||||
final grouped = <String, List<dynamic>>{};
|
||||
for (final c in foldered) {
|
||||
@@ -375,30 +406,41 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
|
||||
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),
|
||||
],
|
||||
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()],
|
||||
),
|
||||
],
|
||||
);
|
||||
}).toList();
|
||||
return sections.isEmpty
|
||||
? [const SizedBox.shrink()]
|
||||
: sections;
|
||||
},
|
||||
loading: () => [const SizedBox.shrink()],
|
||||
error: (e, st) => [const SizedBox.shrink()],
|
||||
),
|
||||
const SizedBox(height: Spacing.md),
|
||||
if (regular.isNotEmpty) ...[
|
||||
_buildSectionHeader(AppLocalizations.of(context)!.recent, regular.length),
|
||||
_buildSectionHeader(
|
||||
AppLocalizations.of(context)!.recent,
|
||||
regular.length,
|
||||
),
|
||||
const SizedBox(height: Spacing.xs),
|
||||
...regular.map(_buildTileFor),
|
||||
],
|
||||
@@ -409,7 +451,8 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
|
||||
],
|
||||
);
|
||||
},
|
||||
loading: () => const Center(child: CircularProgressIndicator(strokeWidth: 2.0)),
|
||||
loading: () =>
|
||||
const Center(child: CircularProgressIndicator(strokeWidth: 2.0)),
|
||||
error: (e, _) => Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(Spacing.md),
|
||||
@@ -442,7 +485,10 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
|
||||
decoration: BoxDecoration(
|
||||
color: theme.surfaceBackground.withValues(alpha: 0.6),
|
||||
borderRadius: BorderRadius.circular(AppBorderRadius.xs),
|
||||
border: Border.all(color: theme.dividerColor, width: BorderWidth.thin),
|
||||
border: Border.all(
|
||||
color: theme.dividerColor,
|
||||
width: BorderWidth.thin,
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
'$count',
|
||||
@@ -461,7 +507,7 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
|
||||
return Row(
|
||||
children: [
|
||||
Text(
|
||||
'Folders',
|
||||
AppLocalizations.of(context)!.folders,
|
||||
style: AppTypography.bodySmallStyle.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: theme.textSecondary,
|
||||
@@ -473,7 +519,9 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
|
||||
visualDensity: VisualDensity.compact,
|
||||
tooltip: AppLocalizations.of(context)!.newFolder,
|
||||
icon: Icon(
|
||||
Platform.isIOS ? CupertinoIcons.folder_badge_plus : Icons.create_new_folder_outlined,
|
||||
Platform.isIOS
|
||||
? CupertinoIcons.folder_badge_plus
|
||||
: Icons.create_new_folder_outlined,
|
||||
color: theme.iconPrimary,
|
||||
),
|
||||
onPressed: _promptCreateFolder,
|
||||
@@ -489,7 +537,10 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
backgroundColor: theme.surfaceBackground,
|
||||
title: Text(AppLocalizations.of(context)!.newFolder, style: TextStyle(color: theme.textPrimary)),
|
||||
title: Text(
|
||||
AppLocalizations.of(context)!.newFolder,
|
||||
style: TextStyle(color: theme.textPrimary),
|
||||
),
|
||||
content: TextField(
|
||||
controller: controller,
|
||||
autofocus: true,
|
||||
@@ -497,8 +548,12 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
|
||||
decoration: InputDecoration(
|
||||
hintText: AppLocalizations.of(context)!.folderName,
|
||||
hintStyle: TextStyle(color: theme.inputPlaceholder),
|
||||
enabledBorder: UnderlineInputBorder(borderSide: BorderSide(color: theme.inputBorder)),
|
||||
focusedBorder: UnderlineInputBorder(borderSide: BorderSide(color: theme.buttonPrimary)),
|
||||
enabledBorder: UnderlineInputBorder(
|
||||
borderSide: BorderSide(color: theme.inputBorder),
|
||||
),
|
||||
focusedBorder: UnderlineInputBorder(
|
||||
borderSide: BorderSide(color: theme.buttonPrimary),
|
||||
),
|
||||
),
|
||||
onSubmitted: (v) => Navigator.pop(ctx, controller.text.trim()),
|
||||
),
|
||||
@@ -527,7 +582,11 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
|
||||
UiUtils.showMessage(context, AppLocalizations.of(context)!.folderCreated);
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
UiUtils.showMessage(context, AppLocalizations.of(context)!.failedToCreateFolder, isError: true);
|
||||
UiUtils.showMessage(
|
||||
context,
|
||||
AppLocalizations.of(context)!.failedToCreateFolder,
|
||||
isError: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -557,12 +616,18 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
|
||||
if (mounted) {
|
||||
UiUtils.showMessage(
|
||||
context,
|
||||
AppLocalizations.of(context)!.movedChatToFolder(details.data.title, name),
|
||||
AppLocalizations.of(
|
||||
context,
|
||||
)!.movedChatToFolder(details.data.title, name),
|
||||
);
|
||||
}
|
||||
} catch (_) {
|
||||
if (mounted) {
|
||||
UiUtils.showMessage(context, AppLocalizations.of(context)!.failedToMoveChat, isError: true);
|
||||
UiUtils.showMessage(
|
||||
context,
|
||||
AppLocalizations.of(context)!.failedToMoveChat,
|
||||
isError: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -596,8 +661,12 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
|
||||
children: [
|
||||
Icon(
|
||||
isExpanded
|
||||
? (Platform.isIOS ? CupertinoIcons.folder_open : Icons.folder_open)
|
||||
: (Platform.isIOS ? CupertinoIcons.folder : Icons.folder),
|
||||
? (Platform.isIOS
|
||||
? CupertinoIcons.folder_open
|
||||
: Icons.folder_open)
|
||||
: (Platform.isIOS
|
||||
? CupertinoIcons.folder
|
||||
: Icons.folder),
|
||||
color: theme.iconPrimary,
|
||||
),
|
||||
const SizedBox(width: Spacing.sm),
|
||||
@@ -619,10 +688,14 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
|
||||
const SizedBox(width: Spacing.xs),
|
||||
Icon(
|
||||
isExpanded
|
||||
? (Platform.isIOS ? CupertinoIcons.chevron_up : Icons.expand_less)
|
||||
: (Platform.isIOS ? CupertinoIcons.chevron_down : Icons.expand_more),
|
||||
? (Platform.isIOS
|
||||
? CupertinoIcons.chevron_up
|
||||
: Icons.expand_less)
|
||||
: (Platform.isIOS
|
||||
? CupertinoIcons.chevron_down
|
||||
: Icons.expand_more),
|
||||
color: theme.iconSecondary,
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -654,11 +727,18 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
|
||||
ref.invalidate(conversationsProvider);
|
||||
ref.invalidate(foldersProvider);
|
||||
if (mounted) {
|
||||
UiUtils.showMessage(context, 'Removed "${details.data.title}" from folder');
|
||||
UiUtils.showMessage(
|
||||
context,
|
||||
'Removed "${details.data.title}" from folder',
|
||||
);
|
||||
}
|
||||
} catch (_) {
|
||||
if (mounted) {
|
||||
UiUtils.showMessage(context, AppLocalizations.of(context)!.failedToMoveChat, isError: true);
|
||||
UiUtils.showMessage(
|
||||
context,
|
||||
AppLocalizations.of(context)!.failedToMoveChat,
|
||||
isError: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -713,7 +793,9 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
|
||||
title: title,
|
||||
pinned: conv.pinned == true,
|
||||
selected: isActive,
|
||||
onTap: _isLoadingConversation ? null : () => _selectConversation(context, conv.id),
|
||||
onTap: _isLoadingConversation
|
||||
? null
|
||||
: () => _selectConversation(context, conv.id),
|
||||
// Remove long-press context menu to avoid conflict with drag gesture
|
||||
onLongPress: null,
|
||||
onMorePressed: () {
|
||||
@@ -723,7 +805,10 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
|
||||
);
|
||||
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(bottom: Spacing.xs, left: inFolder ? Spacing.md : 0),
|
||||
padding: EdgeInsets.only(
|
||||
bottom: Spacing.xs,
|
||||
left: inFolder ? Spacing.md : 0,
|
||||
),
|
||||
child: LongPressDraggable<_DragConversationData>(
|
||||
data: _DragConversationData(id: conv.id, title: title),
|
||||
dragAnchorStrategy: pointerDragAnchorStrategy,
|
||||
@@ -768,7 +853,8 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
|
||||
),
|
||||
onDragStarted: () {
|
||||
HapticFeedback.lightImpact();
|
||||
final hasFolder = (conv.folderId != null && (conv.folderId as String).isNotEmpty);
|
||||
final hasFolder =
|
||||
(conv.folderId != null && (conv.folderId as String).isNotEmpty);
|
||||
setState(() {
|
||||
_isDragging = true;
|
||||
_draggingHasFolder = hasFolder;
|
||||
@@ -794,12 +880,14 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
|
||||
color: theme.surfaceBackground.withValues(alpha: 0.05),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(AppBorderRadius.md),
|
||||
side: BorderSide(color: theme.dividerColor, width: BorderWidth.regular),
|
||||
side: BorderSide(
|
||||
color: theme.dividerColor,
|
||||
width: BorderWidth.regular,
|
||||
),
|
||||
),
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(AppBorderRadius.md),
|
||||
onTap: () =>
|
||||
ref.read(_showArchivedProvider.notifier).state = !show,
|
||||
onTap: () => ref.read(_showArchivedProvider.notifier).state = !show,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: Spacing.md,
|
||||
@@ -816,7 +904,7 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
|
||||
const SizedBox(width: Spacing.sm),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Archived',
|
||||
AppLocalizations.of(context)!.archived,
|
||||
style: AppTypography.bodyLargeStyle.copyWith(
|
||||
color: theme.textPrimary,
|
||||
fontWeight: FontWeight.w600,
|
||||
@@ -833,11 +921,11 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
|
||||
Icon(
|
||||
show
|
||||
? (Platform.isIOS
|
||||
? CupertinoIcons.chevron_up
|
||||
: Icons.expand_less)
|
||||
? CupertinoIcons.chevron_up
|
||||
: Icons.expand_less)
|
||||
: (Platform.isIOS
|
||||
? CupertinoIcons.chevron_down
|
||||
: Icons.expand_more),
|
||||
? CupertinoIcons.chevron_down
|
||||
: Icons.expand_more),
|
||||
color: theme.iconSecondary,
|
||||
),
|
||||
],
|
||||
@@ -867,9 +955,9 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
|
||||
ref.read(activeConversationProvider.notifier).state = full;
|
||||
} else {
|
||||
// Fallback: let ChatPage handle if API missing
|
||||
ref.read(activeConversationProvider.notifier).state =
|
||||
(await ref.read(conversationsProvider.future))
|
||||
.firstWhere((c) => c.id == id);
|
||||
ref.read(activeConversationProvider.notifier).state = (await ref.read(
|
||||
conversationsProvider.future,
|
||||
)).firstWhere((c) => c.id == id);
|
||||
}
|
||||
|
||||
// Clear global loading before closing drawer
|
||||
@@ -890,7 +978,12 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
|
||||
return SafeArea(
|
||||
top: false,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(Spacing.sm, 0, Spacing.sm, Spacing.sm),
|
||||
padding: const EdgeInsets.fromLTRB(
|
||||
Spacing.sm,
|
||||
0,
|
||||
Spacing.sm,
|
||||
Spacing.sm,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
@@ -901,7 +994,10 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
|
||||
decoration: BoxDecoration(
|
||||
color: theme.surfaceBackground.withValues(alpha: 0.04),
|
||||
borderRadius: BorderRadius.circular(AppBorderRadius.md),
|
||||
border: Border.all(color: theme.dividerColor, width: BorderWidth.regular),
|
||||
border: Border.all(
|
||||
color: theme.dividerColor,
|
||||
width: BorderWidth.regular,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
@@ -910,12 +1006,20 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
|
||||
height: IconSize.avatar,
|
||||
decoration: BoxDecoration(
|
||||
color: theme.buttonPrimary.withValues(alpha: 0.15),
|
||||
borderRadius: BorderRadius.circular(AppBorderRadius.avatar),
|
||||
border: Border.all(color: theme.buttonPrimary.withValues(alpha: 0.35), width: BorderWidth.thin),
|
||||
borderRadius: BorderRadius.circular(
|
||||
AppBorderRadius.avatar,
|
||||
),
|
||||
border: Border.all(
|
||||
color: theme.buttonPrimary.withValues(alpha: 0.35),
|
||||
width: BorderWidth.thin,
|
||||
),
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
(user.name ?? user.username ?? 'U').toString().substring(0, 1).toUpperCase(),
|
||||
(user.name ?? user.username ?? 'U')
|
||||
.toString()
|
||||
.substring(0, 1)
|
||||
.toUpperCase(),
|
||||
style: AppTypography.bodyLargeStyle.copyWith(
|
||||
color: theme.buttonPrimary,
|
||||
fontWeight: FontWeight.w700,
|
||||
@@ -949,11 +1053,13 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
|
||||
onPressed: () {
|
||||
Navigator.of(context).maybePop();
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(builder: (_) => const ProfilePage()),
|
||||
MaterialPageRoute(
|
||||
builder: (_) => const ProfilePage(),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Text(AppLocalizations.of(context)!.manage),
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -974,7 +1080,9 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
|
||||
context: context,
|
||||
backgroundColor: theme.surfaceBackground,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(AppBorderRadius.lg)),
|
||||
borderRadius: BorderRadius.vertical(
|
||||
top: Radius.circular(AppBorderRadius.lg),
|
||||
),
|
||||
),
|
||||
builder: (sheetContext) {
|
||||
return SafeArea(
|
||||
@@ -984,8 +1092,12 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
|
||||
ListTile(
|
||||
leading: Icon(
|
||||
isPinned
|
||||
? (Platform.isIOS ? CupertinoIcons.pin_slash : Icons.push_pin_outlined)
|
||||
: (Platform.isIOS ? CupertinoIcons.pin_fill : Icons.push_pin_rounded),
|
||||
? (Platform.isIOS
|
||||
? CupertinoIcons.pin_slash
|
||||
: Icons.push_pin_outlined)
|
||||
: (Platform.isIOS
|
||||
? CupertinoIcons.pin_fill
|
||||
: Icons.push_pin_rounded),
|
||||
color: theme.iconPrimary,
|
||||
),
|
||||
title: Text(
|
||||
@@ -1001,15 +1113,23 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
|
||||
await chat.pinConversation(ref, conv.id, !isPinned);
|
||||
} catch (_) {
|
||||
if (!mounted) return;
|
||||
UiUtils.showMessage(this.context, AppLocalizations.of(context)!.failedToUpdatePin, isError: true);
|
||||
UiUtils.showMessage(
|
||||
this.context,
|
||||
AppLocalizations.of(context)!.failedToUpdatePin,
|
||||
isError: true,
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: Icon(
|
||||
isArchived
|
||||
? (Platform.isIOS ? CupertinoIcons.archivebox_fill : Icons.unarchive_rounded)
|
||||
: (Platform.isIOS ? CupertinoIcons.archivebox : Icons.archive_rounded),
|
||||
? (Platform.isIOS
|
||||
? CupertinoIcons.archivebox_fill
|
||||
: Icons.unarchive_rounded)
|
||||
: (Platform.isIOS
|
||||
? CupertinoIcons.archivebox
|
||||
: Icons.archive_rounded),
|
||||
color: theme.iconPrimary,
|
||||
),
|
||||
title: Text(
|
||||
@@ -1025,7 +1145,11 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
|
||||
await chat.archiveConversation(ref, conv.id, !isArchived);
|
||||
} catch (_) {
|
||||
if (!mounted) return;
|
||||
UiUtils.showMessage(this.context, AppLocalizations.of(context)!.failedToUpdateArchive, isError: true);
|
||||
UiUtils.showMessage(
|
||||
this.context,
|
||||
AppLocalizations.of(context)!.failedToUpdateArchive,
|
||||
isError: true,
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
@@ -1034,7 +1158,10 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
|
||||
Platform.isIOS ? CupertinoIcons.pencil : Icons.edit_rounded,
|
||||
color: theme.iconPrimary,
|
||||
),
|
||||
title: Text(AppLocalizations.of(context)!.rename, style: TextStyle(color: theme.textPrimary)),
|
||||
title: Text(
|
||||
AppLocalizations.of(context)!.rename,
|
||||
style: TextStyle(color: theme.textPrimary),
|
||||
),
|
||||
onTap: () async {
|
||||
HapticFeedback.selectionClick();
|
||||
Navigator.pop(sheetContext);
|
||||
@@ -1047,7 +1174,10 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
|
||||
Platform.isIOS ? CupertinoIcons.delete : Icons.delete_rounded,
|
||||
color: theme.error,
|
||||
),
|
||||
title: Text(AppLocalizations.of(context)!.delete, style: TextStyle(color: theme.error)),
|
||||
title: Text(
|
||||
AppLocalizations.of(context)!.delete,
|
||||
style: TextStyle(color: theme.error),
|
||||
),
|
||||
onTap: () async {
|
||||
HapticFeedback.mediumImpact();
|
||||
Navigator.pop(sheetContext);
|
||||
@@ -1074,7 +1204,10 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
|
||||
builder: (dialogContext) {
|
||||
return AlertDialog(
|
||||
backgroundColor: theme.surfaceBackground,
|
||||
title: Text(AppLocalizations.of(context)!.renameChat, style: TextStyle(color: theme.textPrimary)),
|
||||
title: Text(
|
||||
AppLocalizations.of(context)!.renameChat,
|
||||
style: TextStyle(color: theme.textPrimary),
|
||||
),
|
||||
content: TextField(
|
||||
controller: controller,
|
||||
autofocus: true,
|
||||
@@ -1121,12 +1254,17 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
|
||||
ref.invalidate(conversationsProvider);
|
||||
final active = ref.read(activeConversationProvider);
|
||||
if (active?.id == conversationId) {
|
||||
ref.read(activeConversationProvider.notifier).state =
|
||||
active!.copyWith(title: newName);
|
||||
ref.read(activeConversationProvider.notifier).state = active!.copyWith(
|
||||
title: newName,
|
||||
);
|
||||
}
|
||||
} catch (_) {
|
||||
if (!mounted) return;
|
||||
UiUtils.showMessage(this.context, AppLocalizations.of(context)!.failedToRenameChat, isError: true);
|
||||
UiUtils.showMessage(
|
||||
this.context,
|
||||
AppLocalizations.of(context)!.failedToRenameChat,
|
||||
isError: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1157,7 +1295,11 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
|
||||
ref.invalidate(conversationsProvider);
|
||||
} catch (_) {
|
||||
if (!mounted) return;
|
||||
UiUtils.showMessage(this.context, AppLocalizations.of(context)!.failedToDeleteChat, isError: true);
|
||||
UiUtils.showMessage(
|
||||
this.context,
|
||||
AppLocalizations.of(context)!.failedToDeleteChat,
|
||||
isError: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1195,7 +1337,9 @@ class _ConversationTile extends StatelessWidget {
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(AppBorderRadius.md),
|
||||
side: BorderSide(
|
||||
color: selected ? theme.buttonPrimary.withValues(alpha: 0.5) : theme.dividerColor,
|
||||
color: selected
|
||||
? theme.buttonPrimary.withValues(alpha: 0.5)
|
||||
: theme.dividerColor,
|
||||
width: BorderWidth.regular,
|
||||
),
|
||||
),
|
||||
@@ -1226,9 +1370,14 @@ class _ConversationTile extends StatelessWidget {
|
||||
IconButton(
|
||||
visualDensity: VisualDensity.compact,
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(minWidth: 36, minHeight: 36),
|
||||
constraints: const BoxConstraints(
|
||||
minWidth: 36,
|
||||
minHeight: 36,
|
||||
),
|
||||
icon: Icon(
|
||||
Platform.isIOS ? CupertinoIcons.ellipsis : Icons.more_vert_rounded,
|
||||
Platform.isIOS
|
||||
? CupertinoIcons.ellipsis
|
||||
: Icons.more_vert_rounded,
|
||||
color: theme.iconSecondary,
|
||||
size: IconSize.md,
|
||||
),
|
||||
|
||||
@@ -119,7 +119,9 @@ class ProfilePage extends ConsumerWidget {
|
||||
centerTitle: true,
|
||||
),
|
||||
body: Center(
|
||||
child: ImprovedLoadingState(message: AppLocalizations.of(context)!.loadingProfile),
|
||||
child: ImprovedLoadingState(
|
||||
message: AppLocalizations.of(context)!.loadingProfile,
|
||||
),
|
||||
),
|
||||
),
|
||||
error: (error, stack) => Scaffold(
|
||||
@@ -311,17 +313,16 @@ class ProfilePage extends ConsumerWidget {
|
||||
Widget _buildDefaultModelTile(BuildContext context, WidgetRef ref) {
|
||||
final settings = ref.watch(appSettingsProvider);
|
||||
final modelsAsync = ref.watch(modelsProvider);
|
||||
|
||||
|
||||
return modelsAsync.when(
|
||||
data: (models) {
|
||||
final currentModel = models.firstWhere(
|
||||
(m) => m.id == settings.defaultModel,
|
||||
orElse: () => models.isNotEmpty ? models.first : const Model(
|
||||
id: 'none',
|
||||
name: 'No models available',
|
||||
),
|
||||
orElse: () => models.isNotEmpty
|
||||
? models.first
|
||||
: const Model(id: 'none', name: 'No models available'),
|
||||
);
|
||||
|
||||
|
||||
return ListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: Spacing.listItemPadding,
|
||||
@@ -352,7 +353,9 @@ class ProfilePage extends ConsumerWidget {
|
||||
),
|
||||
),
|
||||
subtitle: Text(
|
||||
settings.defaultModel != null ? currentModel.name : AppLocalizations.of(context)!.autoSelect,
|
||||
settings.defaultModel != null
|
||||
? currentModel.name
|
||||
: AppLocalizations.of(context)!.autoSelect,
|
||||
style: context.conduitTheme.bodySmall?.copyWith(
|
||||
color: context.conduitTheme.textSecondary,
|
||||
),
|
||||
@@ -484,7 +487,7 @@ class ProfilePage extends ConsumerWidget {
|
||||
),
|
||||
),
|
||||
title: Text(
|
||||
AppLocalizations.of(context)!.menuItem,
|
||||
AppLocalizations.of(context)!.appLanguage,
|
||||
style: context.conduitTheme.bodyLarge?.copyWith(
|
||||
color: context.conduitTheme.textPrimary,
|
||||
fontWeight: FontWeight.w500,
|
||||
@@ -600,7 +603,7 @@ class ProfilePage extends ConsumerWidget {
|
||||
),
|
||||
),
|
||||
title: Text(
|
||||
'Dark Mode',
|
||||
AppLocalizations.of(context)!.darkMode,
|
||||
style: context.conduitTheme.bodyLarge?.copyWith(
|
||||
color: context.conduitTheme.textPrimary,
|
||||
fontWeight: FontWeight.w500,
|
||||
@@ -720,7 +723,11 @@ class ProfilePage extends ConsumerWidget {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _showModelSelector(BuildContext context, WidgetRef ref, List<Model> models) async {
|
||||
Future<void> _showModelSelector(
|
||||
BuildContext context,
|
||||
WidgetRef ref,
|
||||
List<Model> models,
|
||||
) async {
|
||||
final result = await showModalBottomSheet<String?>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
@@ -730,13 +737,15 @@ class ProfilePage extends ConsumerWidget {
|
||||
currentDefaultModelId: ref.read(appSettingsProvider).defaultModel,
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
// result is non-null only when Save button is pressed
|
||||
// null means the sheet was dismissed without saving
|
||||
if (result != null) {
|
||||
// Handle special case: 'auto-select' should be stored as null
|
||||
final modelIdToSave = result == 'auto-select' ? null : result;
|
||||
await ref.read(appSettingsProvider.notifier).setDefaultModel(modelIdToSave);
|
||||
await ref
|
||||
.read(appSettingsProvider.notifier)
|
||||
.setDefaultModel(modelIdToSave);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -765,10 +774,12 @@ class _DefaultModelBottomSheet extends ConsumerStatefulWidget {
|
||||
});
|
||||
|
||||
@override
|
||||
ConsumerState<_DefaultModelBottomSheet> createState() => _DefaultModelBottomSheetState();
|
||||
ConsumerState<_DefaultModelBottomSheet> createState() =>
|
||||
_DefaultModelBottomSheetState();
|
||||
}
|
||||
|
||||
class _DefaultModelBottomSheetState extends ConsumerState<_DefaultModelBottomSheet> {
|
||||
class _DefaultModelBottomSheetState
|
||||
extends ConsumerState<_DefaultModelBottomSheet> {
|
||||
final TextEditingController _searchController = TextEditingController();
|
||||
String _searchQuery = '';
|
||||
List<Model> _filteredModels = [];
|
||||
@@ -833,7 +844,7 @@ class _DefaultModelBottomSheetState extends ConsumerState<_DefaultModelBottomShe
|
||||
const Model(id: 'auto-select', name: 'Auto-select'),
|
||||
...widget.models,
|
||||
];
|
||||
|
||||
|
||||
if (_searchQuery.isNotEmpty) {
|
||||
_filteredModels = allModels.where((model) {
|
||||
return model.name.toLowerCase().contains(_searchQuery) ||
|
||||
@@ -896,18 +907,24 @@ class _DefaultModelBottomSheetState extends ConsumerState<_DefaultModelBottomShe
|
||||
filled: true,
|
||||
fillColor: context.conduitTheme.inputBackground,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(AppBorderRadius.md),
|
||||
borderRadius: BorderRadius.circular(
|
||||
AppBorderRadius.md,
|
||||
),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(AppBorderRadius.md),
|
||||
borderRadius: BorderRadius.circular(
|
||||
AppBorderRadius.md,
|
||||
),
|
||||
borderSide: BorderSide(
|
||||
color: context.conduitTheme.inputBorder,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(AppBorderRadius.md),
|
||||
borderRadius: BorderRadius.circular(
|
||||
AppBorderRadius.md,
|
||||
),
|
||||
borderSide: BorderSide(
|
||||
color: context.conduitTheme.buttonPrimary,
|
||||
width: 1,
|
||||
@@ -937,10 +954,16 @@ class _DefaultModelBottomSheetState extends ConsumerState<_DefaultModelBottomShe
|
||||
),
|
||||
const SizedBox(width: Spacing.xs),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 6,
|
||||
vertical: 2,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: context.conduitTheme.surfaceBackground.withValues(alpha: 0.6),
|
||||
borderRadius: BorderRadius.circular(AppBorderRadius.xs),
|
||||
color: context.conduitTheme.surfaceBackground
|
||||
.withValues(alpha: 0.6),
|
||||
borderRadius: BorderRadius.circular(
|
||||
AppBorderRadius.xs,
|
||||
),
|
||||
border: Border.all(
|
||||
color: context.conduitTheme.dividerColor,
|
||||
width: BorderWidth.thin,
|
||||
@@ -993,8 +1016,9 @@ class _DefaultModelBottomSheetState extends ConsumerState<_DefaultModelBottomShe
|
||||
itemBuilder: (context, index) {
|
||||
final model = _filteredModels[index];
|
||||
final isAutoSelect = model.id == 'auto-select';
|
||||
final isSelected = isAutoSelect
|
||||
? _selectedModelId == null || _selectedModelId == 'auto-select'
|
||||
final isSelected = isAutoSelect
|
||||
? _selectedModelId == null ||
|
||||
_selectedModelId == 'auto-select'
|
||||
: _selectedModelId == model.id;
|
||||
|
||||
return _buildModelListTile(
|
||||
@@ -1003,8 +1027,9 @@ class _DefaultModelBottomSheetState extends ConsumerState<_DefaultModelBottomShe
|
||||
isAutoSelect: isAutoSelect,
|
||||
onTap: () {
|
||||
HapticFeedback.lightImpact();
|
||||
final selectedId =
|
||||
isAutoSelect ? 'auto-select' : model.id;
|
||||
final selectedId = isAutoSelect
|
||||
? 'auto-select'
|
||||
: model.id;
|
||||
// Return selection immediately; caller handles persisting
|
||||
Navigator.pop(context, selectedId);
|
||||
},
|
||||
@@ -1070,13 +1095,19 @@ class _DefaultModelBottomSheetState extends ConsumerState<_DefaultModelBottomShe
|
||||
width: 32,
|
||||
height: 32,
|
||||
decoration: BoxDecoration(
|
||||
color: context.conduitTheme.buttonPrimary.withValues(alpha: 0.15),
|
||||
color: context.conduitTheme.buttonPrimary.withValues(
|
||||
alpha: 0.15,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(AppBorderRadius.md),
|
||||
),
|
||||
child: Icon(
|
||||
isAutoSelect
|
||||
? (Platform.isIOS ? CupertinoIcons.wand_stars : Icons.auto_awesome)
|
||||
: (Platform.isIOS ? CupertinoIcons.cube : Icons.psychology),
|
||||
isAutoSelect
|
||||
? (Platform.isIOS
|
||||
? CupertinoIcons.wand_stars
|
||||
: Icons.auto_awesome)
|
||||
: (Platform.isIOS
|
||||
? CupertinoIcons.cube
|
||||
: Icons.psychology),
|
||||
color: context.conduitTheme.buttonPrimary,
|
||||
size: 16,
|
||||
),
|
||||
@@ -1087,7 +1118,9 @@ class _DefaultModelBottomSheetState extends ConsumerState<_DefaultModelBottomShe
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
isAutoSelect ? AppLocalizations.of(context)!.autoSelect : model.name,
|
||||
isAutoSelect
|
||||
? AppLocalizations.of(context)!.autoSelect
|
||||
: model.name,
|
||||
style: TextStyle(
|
||||
color: context.conduitTheme.textPrimary,
|
||||
fontWeight: FontWeight.w600,
|
||||
@@ -1143,13 +1176,17 @@ class _DefaultModelBottomSheetState extends ConsumerState<_DefaultModelBottomShe
|
||||
borderRadius: BorderRadius.circular(AppBorderRadius.md),
|
||||
border: Border.all(
|
||||
color: isSelected
|
||||
? context.conduitTheme.buttonPrimary.withValues(alpha: 0.6)
|
||||
? context.conduitTheme.buttonPrimary.withValues(
|
||||
alpha: 0.6,
|
||||
)
|
||||
: context.conduitTheme.dividerColor,
|
||||
),
|
||||
),
|
||||
child: Icon(
|
||||
isSelected
|
||||
? (Platform.isIOS ? CupertinoIcons.check_mark : Icons.check)
|
||||
? (Platform.isIOS
|
||||
? CupertinoIcons.check_mark
|
||||
: Icons.check)
|
||||
: (Platform.isIOS ? CupertinoIcons.add : Icons.add),
|
||||
color: isSelected
|
||||
? context.conduitTheme.textInverse
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:conduit/l10n/app_localizations.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
@@ -59,9 +60,10 @@ class _UnifiedToolsModalState extends ConsumerState<UnifiedToolsModal> {
|
||||
Column(
|
||||
children: [
|
||||
_buildFeatureTile(
|
||||
title: 'Web Search',
|
||||
description:
|
||||
'Let the assistant search the internet while answering.',
|
||||
title: AppLocalizations.of(context)!.webSearch,
|
||||
description: AppLocalizations.of(
|
||||
context,
|
||||
)!.webSearchDescription,
|
||||
icon: Platform.isIOS
|
||||
? CupertinoIcons.search
|
||||
: Icons.search,
|
||||
@@ -74,9 +76,10 @@ class _UnifiedToolsModalState extends ConsumerState<UnifiedToolsModal> {
|
||||
),
|
||||
if (imageGenAvailable)
|
||||
_buildFeatureTile(
|
||||
title: 'Image Generation',
|
||||
description:
|
||||
'Generate images from your prompt and attach them.',
|
||||
title: AppLocalizations.of(context)!.imageGeneration,
|
||||
description: AppLocalizations.of(
|
||||
context,
|
||||
)!.imageGenerationDescription,
|
||||
icon: Platform.isIOS
|
||||
? CupertinoIcons.photo
|
||||
: Icons.image,
|
||||
|
||||
Reference in New Issue
Block a user