diff --git a/lib/core/models/user_settings.dart b/lib/core/models/user_settings.dart index 4f50a53..e217dad 100644 --- a/lib/core/models/user_settings.dart +++ b/lib/core/models/user_settings.dart @@ -31,6 +31,9 @@ sealed class UserSettings with _$UserSettings { @Default(false) bool reduceMotion, @Default(true) bool hapticFeedback, + // Model preferences + String? defaultModelId, + // Advanced settings @Default({}) Map customSettings, }) = _UserSettings; diff --git a/lib/core/providers/app_providers.dart b/lib/core/providers/app_providers.dart index 10dd384..3c608fa 100644 --- a/lib/core/providers/app_providers.dart +++ b/lib/core/providers/app_providers.dart @@ -18,6 +18,7 @@ import '../models/folder.dart'; import '../models/user_settings.dart'; import '../models/file_info.dart'; import '../models/knowledge_base.dart'; +import '../services/settings_service.dart'; import '../services/optimized_storage_service.dart'; // Storage providers @@ -500,8 +501,10 @@ final loadConversationProvider = FutureProvider.family(( return fullConversation; }); -// Provider to automatically load and set the default model from OpenWebUI +// Provider to automatically load and set the default model from user settings or OpenWebUI final defaultModelProvider = FutureProvider((ref) async { + // Watch user settings to refresh when default model changes + ref.watch(appSettingsProvider); // Handle reviewer mode first final reviewerMode = ref.watch(reviewerModeProvider); if (reviewerMode) { @@ -562,45 +565,71 @@ final defaultModelProvider = FutureProvider((ref) async { Model? selectedModel; - // Try to get the server's default model configuration - try { - final defaultModelId = await api.getDefaultModel(); + // First check user's preferred default model + final userSettings = ref.read(appSettingsProvider); + final userDefaultModelId = userSettings.defaultModel; + + if (userDefaultModelId != null && userDefaultModelId.isNotEmpty) { + try { + selectedModel = models.firstWhere( + (model) => + model.id == userDefaultModelId || + model.name == userDefaultModelId || + model.id.contains(userDefaultModelId) || + model.name.contains(userDefaultModelId), + ); + foundation.debugPrint( + 'DEBUG: Found user default model: ${selectedModel.name}', + ); + } catch (e) { + foundation.debugPrint( + 'DEBUG: User default model "$userDefaultModelId" not found in available models', + ); + selectedModel = null; // Will fall back to server default or first model + } + } - if (defaultModelId != null && defaultModelId.isNotEmpty) { - // Find the model that matches the default model ID - try { - selectedModel = models.firstWhere( - (model) => - model.id == defaultModelId || - model.name == defaultModelId || - model.id.contains(defaultModelId) || - model.name.contains(defaultModelId), - ); - foundation.debugPrint( - 'DEBUG: Found server default model: ${selectedModel.name}', - ); - } catch (e) { - foundation.debugPrint( - 'DEBUG: Default model "$defaultModelId" not found in available models', - ); + // If no user default or user default not found, try server's default model + if (selectedModel == null) { + try { + final defaultModelId = await api.getDefaultModel(); + + if (defaultModelId != null && defaultModelId.isNotEmpty) { + // Find the model that matches the default model ID + try { + selectedModel = models.firstWhere( + (model) => + model.id == defaultModelId || + model.name == defaultModelId || + model.id.contains(defaultModelId) || + model.name.contains(defaultModelId), + ); + foundation.debugPrint( + 'DEBUG: Found server default model: ${selectedModel.name}', + ); + } catch (e) { + foundation.debugPrint( + 'DEBUG: Server default model "$defaultModelId" not found in available models', + ); + selectedModel = models.first; + } + } else { + // No server default, use first available model selectedModel = models.first; + foundation.debugPrint( + 'DEBUG: No server default model, using first available: ${selectedModel.name}', + ); } - } else { - // No server default, use first available model + } catch (apiError) { + foundation.debugPrint( + 'DEBUG: Failed to get default model from server: $apiError', + ); + // Use first available model as fallback selectedModel = models.first; foundation.debugPrint( - 'DEBUG: No server default model, using first available: ${selectedModel.name}', + 'DEBUG: Using first available model as fallback: ${selectedModel.name}', ); } - } catch (apiError) { - foundation.debugPrint( - 'DEBUG: Failed to get default model from server: $apiError', - ); - // Use first available model as fallback - selectedModel = models.first; - foundation.debugPrint( - 'DEBUG: Using first available model as fallback: ${selectedModel.name}', - ); } // Defer the state update to avoid modifying providers during initialization diff --git a/lib/core/services/navigation_service.dart b/lib/core/services/navigation_service.dart index 9a32177..0c587a2 100644 --- a/lib/core/services/navigation_service.dart +++ b/lib/core/services/navigation_service.dart @@ -3,7 +3,7 @@ import 'package:flutter/services.dart'; // ThemedDialogs handles theming; no direct use of extensions here import '../../features/chat/views/chat_page.dart'; import '../../features/auth/views/connect_signin_page.dart'; -import '../../features/settings/views/searchable_settings_page.dart'; + import '../../features/profile/views/profile_page.dart'; import '../../features/files/views/files_page.dart'; @@ -148,10 +148,7 @@ class NavigationService { return navigateTo(Routes.login, clearStack: true); } - /// Navigate to settings - static Future navigateToSettings() { - return navigateTo(Routes.settings); - } + /// Navigate to profile static Future navigateToProfile() { @@ -202,9 +199,7 @@ class NavigationService { page = const ConnectAndSignInPage(); break; - case Routes.settings: - page = const SearchableSettingsPage(); - break; + case Routes.profile: page = const ProfilePage(); @@ -244,7 +239,7 @@ class NavigationService { class Routes { static const String chat = '/chat'; static const String login = '/login'; - static const String settings = '/settings'; + static const String profile = '/profile'; static const String serverConnection = '/server-connection'; static const String search = '/search'; diff --git a/lib/core/services/settings_service.dart b/lib/core/services/settings_service.dart index 7f57a75..5af0b27 100644 --- a/lib/core/services/settings_service.dart +++ b/lib/core/services/settings_service.dart @@ -11,6 +11,7 @@ class SettingsService { static const String _highContrastKey = 'high_contrast'; static const String _largeTextKey = 'large_text'; static const String _darkModeKey = 'dark_mode'; + static const String _defaultModelKey = 'default_model'; /// Get reduced motion preference static Future getReduceMotion() async { @@ -84,6 +85,22 @@ class SettingsService { await prefs.setBool(_darkModeKey, value); } + /// Get default model preference + static Future getDefaultModel() async { + final prefs = await SharedPreferences.getInstance(); + return prefs.getString(_defaultModelKey); + } + + /// Set default model preference + static Future setDefaultModel(String? modelId) async { + final prefs = await SharedPreferences.getInstance(); + if (modelId != null) { + await prefs.setString(_defaultModelKey, modelId); + } else { + await prefs.remove(_defaultModelKey); + } + } + /// Load all settings static Future loadSettings() async { return AppSettings( @@ -93,6 +110,7 @@ class SettingsService { highContrast: await getHighContrast(), largeText: await getLargeText(), darkMode: await getDarkMode(), + defaultModel: await getDefaultModel(), ); } @@ -105,6 +123,7 @@ class SettingsService { setHighContrast(settings.highContrast), setLargeText(settings.largeText), setDarkMode(settings.darkMode), + setDefaultModel(settings.defaultModel), ]); } @@ -151,6 +170,7 @@ class AppSettings { final bool highContrast; final bool largeText; final bool darkMode; + final String? defaultModel; const AppSettings({ this.reduceMotion = false, @@ -159,6 +179,7 @@ class AppSettings { this.highContrast = false, this.largeText = false, this.darkMode = true, + this.defaultModel, }); AppSettings copyWith({ @@ -168,6 +189,7 @@ class AppSettings { bool? highContrast, bool? largeText, bool? darkMode, + String? defaultModel, }) { return AppSettings( reduceMotion: reduceMotion ?? this.reduceMotion, @@ -176,6 +198,7 @@ class AppSettings { highContrast: highContrast ?? this.highContrast, largeText: largeText ?? this.largeText, darkMode: darkMode ?? this.darkMode, + defaultModel: defaultModel ?? this.defaultModel, ); } @@ -188,7 +211,8 @@ class AppSettings { other.hapticFeedback == hapticFeedback && other.highContrast == highContrast && other.largeText == largeText && - other.darkMode == darkMode; + other.darkMode == darkMode && + other.defaultModel == defaultModel; } @override @@ -200,6 +224,7 @@ class AppSettings { highContrast, largeText, darkMode, + defaultModel, ); } } @@ -250,6 +275,11 @@ class AppSettingsNotifier extends StateNotifier { await SettingsService.setDarkMode(value); } + Future setDefaultModel(String? modelId) async { + state = state.copyWith(defaultModel: modelId); + await SettingsService.setDefaultModel(modelId); + } + Future resetToDefaults() async { const defaultSettings = AppSettings(); await SettingsService.saveSettings(defaultSettings); diff --git a/lib/features/profile/views/profile_page.dart b/lib/features/profile/views/profile_page.dart index 86a5ff8..0ad2705 100644 --- a/lib/features/profile/views/profile_page.dart +++ b/lib/features/profile/views/profile_page.dart @@ -12,6 +12,11 @@ import '../../../shared/utils/ui_utils.dart'; import '../../../shared/widgets/conduit_components.dart'; import '../../../core/providers/app_providers.dart'; import '../../auth/providers/unified_auth_providers.dart'; +import '../../../core/services/settings_service.dart'; +import '../../../core/models/model.dart'; +import 'dart:async'; +import 'dart:io'; +import '../../chat/views/chat_page_helpers.dart'; /// Profile page (You tab) showing user info and main actions /// Enhanced with production-grade design tokens for better cohesion @@ -263,6 +268,8 @@ class ProfilePage extends ConsumerWidget { padding: EdgeInsets.zero, child: Column( children: [ + _buildDefaultModelTile(context, ref), + Divider(color: context.conduitTheme.dividerColor, height: 1), _buildThemeToggleTile(context, ref), Divider(color: context.conduitTheme.dividerColor, height: 1), _buildAboutTile(context), @@ -343,6 +350,141 @@ 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', + ), + ); + + return ListTile( + contentPadding: const EdgeInsets.symmetric( + horizontal: Spacing.listItemPadding, + vertical: Spacing.sm, + ), + leading: Container( + padding: const EdgeInsets.all(Spacing.sm), + decoration: BoxDecoration( + color: context.conduitTheme.buttonPrimary.withValues( + alpha: Alpha.highlight, + ), + borderRadius: BorderRadius.circular(AppBorderRadius.small), + ), + child: Icon( + UiUtils.platformIcon( + ios: CupertinoIcons.cube_box, + android: Icons.psychology, + ), + color: context.conduitTheme.buttonPrimary, + size: IconSize.medium, + ), + ), + title: Text( + 'Default Model', + style: context.conduitTheme.bodyLarge?.copyWith( + color: context.conduitTheme.textPrimary, + fontWeight: FontWeight.w500, + ), + ), + subtitle: Text( + settings.defaultModel != null ? currentModel.name : 'Auto-select', + style: context.conduitTheme.bodySmall?.copyWith( + color: context.conduitTheme.textSecondary, + ), + ), + trailing: Icon( + UiUtils.platformIcon( + ios: CupertinoIcons.chevron_right, + android: Icons.chevron_right, + ), + color: context.conduitTheme.iconSecondary, + size: IconSize.small, + ), + onTap: () => _showModelSelector(context, ref, models), + ); + }, + loading: () => ListTile( + contentPadding: const EdgeInsets.symmetric( + horizontal: Spacing.listItemPadding, + vertical: Spacing.sm, + ), + leading: Container( + padding: const EdgeInsets.all(Spacing.sm), + decoration: BoxDecoration( + color: context.conduitTheme.buttonPrimary.withValues( + alpha: Alpha.highlight, + ), + borderRadius: BorderRadius.circular(AppBorderRadius.small), + ), + child: Icon( + UiUtils.platformIcon( + ios: CupertinoIcons.cube_box, + android: Icons.psychology, + ), + color: context.conduitTheme.buttonPrimary, + size: IconSize.medium, + ), + ), + title: Text( + 'Default Model', + style: context.conduitTheme.bodyLarge?.copyWith( + color: context.conduitTheme.textPrimary, + fontWeight: FontWeight.w500, + ), + ), + subtitle: Text( + 'Loading models...', + style: context.conduitTheme.bodySmall?.copyWith( + color: context.conduitTheme.textSecondary, + ), + ), + ), + error: (error, stack) => ListTile( + contentPadding: const EdgeInsets.symmetric( + horizontal: Spacing.listItemPadding, + vertical: Spacing.sm, + ), + leading: Container( + padding: const EdgeInsets.all(Spacing.sm), + decoration: BoxDecoration( + color: context.conduitTheme.error.withValues( + alpha: Alpha.highlight, + ), + borderRadius: BorderRadius.circular(AppBorderRadius.small), + ), + child: Icon( + UiUtils.platformIcon( + ios: CupertinoIcons.exclamationmark_triangle, + android: Icons.error_outline, + ), + color: context.conduitTheme.error, + size: IconSize.medium, + ), + ), + title: Text( + 'Default Model', + style: context.conduitTheme.bodyLarge?.copyWith( + color: context.conduitTheme.textPrimary, + fontWeight: FontWeight.w500, + ), + ), + subtitle: Text( + 'Failed to load models', + style: context.conduitTheme.bodySmall?.copyWith( + color: context.conduitTheme.error, + ), + ), + ), + ); + } + Widget _buildThemeToggleTile(BuildContext context, WidgetRef ref) { final themeMode = ref.watch(themeModeProvider); final platformBrightness = MediaQuery.platformBrightnessOf(context); @@ -494,6 +636,22 @@ class ProfilePage extends ConsumerWidget { } } + Future _showModelSelector(BuildContext context, WidgetRef ref, List models) async { + final result = await showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (ctx) => _DefaultModelBottomSheet( + models: models, + currentDefaultModelId: ref.read(appSettingsProvider).defaultModel, + ), + ); + + if (result is String || result == null) { + await ref.read(appSettingsProvider.notifier).setDefaultModel(result); + } + } + void _signOut(BuildContext context, WidgetRef ref) async { final confirm = await UiUtils.showConfirmationDialog( context, @@ -508,3 +666,412 @@ class ProfilePage extends ConsumerWidget { } } } + +class _DefaultModelBottomSheet extends ConsumerStatefulWidget { + final List models; + final String? currentDefaultModelId; + + const _DefaultModelBottomSheet({ + required this.models, + required this.currentDefaultModelId, + }); + + @override + ConsumerState<_DefaultModelBottomSheet> createState() => _DefaultModelBottomSheetState(); +} + +class _DefaultModelBottomSheetState extends ConsumerState<_DefaultModelBottomSheet> { + final TextEditingController _searchController = TextEditingController(); + String _searchQuery = ''; + List _filteredModels = []; + Timer? _searchDebounce; + String? _selectedModelId; + + Widget _capabilityChip({required IconData icon, required String label}) { + return Container( + margin: const EdgeInsets.only(right: Spacing.xs), + padding: const EdgeInsets.symmetric(horizontal: Spacing.xs, vertical: 2), + decoration: BoxDecoration( + color: context.conduitTheme.buttonPrimary.withValues(alpha: 0.08), + borderRadius: BorderRadius.circular(AppBorderRadius.chip), + border: Border.all( + color: context.conduitTheme.buttonPrimary.withValues(alpha: 0.3), + width: BorderWidth.thin, + ), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(icon, size: 12, color: context.conduitTheme.buttonPrimary), + const SizedBox(width: 4), + Text( + label, + style: TextStyle( + fontSize: AppTypography.labelSmall, + color: context.conduitTheme.textSecondary, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ); + } + + @override + void initState() { + super.initState(); + _selectedModelId = widget.currentDefaultModelId; + // Add auto-select as first item + _filteredModels = [ + const Model(id: 'auto-select', name: 'Auto-select'), + ...widget.models, + ]; + } + + @override + void dispose() { + _searchController.dispose(); + _searchDebounce?.cancel(); + super.dispose(); + } + + void _filterModels(String query) { + _searchDebounce?.cancel(); + _searchDebounce = Timer(const Duration(milliseconds: 160), () { + setState(() { + _searchQuery = query.toLowerCase(); + List allModels = [ + const Model(id: 'auto-select', name: 'Auto-select'), + ...widget.models, + ]; + + if (_searchQuery.isNotEmpty) { + _filteredModels = allModels.where((model) { + return model.name.toLowerCase().contains(_searchQuery) || + model.id.toLowerCase().contains(_searchQuery); + }).toList(); + } else { + _filteredModels = allModels; + } + }); + }); + } + + @override + Widget build(BuildContext context) { + return DraggableScrollableSheet( + initialChildSize: 0.75, + maxChildSize: 0.92, + minChildSize: 0.45, + builder: (context, scrollController) { + return Container( + decoration: BoxDecoration( + color: context.conduitTheme.surfaceBackground, + borderRadius: const BorderRadius.vertical( + top: Radius.circular(AppBorderRadius.bottomSheet), + ), + border: Border.all( + color: context.conduitTheme.dividerColor, + width: BorderWidth.regular, + ), + boxShadow: ConduitShadows.modal, + ), + child: SafeArea( + top: false, + bottom: true, + child: Padding( + padding: const EdgeInsets.all(Spacing.bottomSheetPadding), + child: Column( + children: [ + // Handle bar + Container( + margin: const EdgeInsets.only( + top: Spacing.sm, + bottom: Spacing.md, + ), + width: Spacing.xxl, + height: Spacing.xs, + decoration: BoxDecoration( + color: context.conduitTheme.dividerColor, + borderRadius: BorderRadius.circular(AppBorderRadius.xs), + ), + ), + + // Header + Padding( + padding: const EdgeInsets.only(bottom: Spacing.md), + child: Row( + children: [ + Expanded( + child: Text( + 'Default Model', + style: context.conduitTheme.headingMedium?.copyWith( + color: context.conduitTheme.textPrimary, + fontWeight: FontWeight.w600, + ), + ), + ), + TextButton( + onPressed: () => Navigator.pop(context, _selectedModelId == 'auto-select' ? null : _selectedModelId), + child: Text( + 'Save', + style: context.conduitTheme.bodyMedium?.copyWith( + color: context.conduitTheme.buttonPrimary, + fontWeight: FontWeight.w600, + ), + ), + ), + ], + ), + ), + + // Search field + Padding( + padding: const EdgeInsets.only(bottom: Spacing.md), + child: TextField( + controller: _searchController, + style: TextStyle(color: context.conduitTheme.textPrimary), + decoration: InputDecoration( + hintText: 'Search...', + hintStyle: TextStyle( + color: context.conduitTheme.inputPlaceholder, + ), + prefixIcon: Icon( + Platform.isIOS ? CupertinoIcons.search : Icons.search, + color: context.conduitTheme.iconSecondary, + ), + filled: true, + fillColor: context.conduitTheme.inputBackground, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(AppBorderRadius.md), + borderSide: BorderSide.none, + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(AppBorderRadius.md), + borderSide: BorderSide( + color: context.conduitTheme.inputBorder, + width: 1, + ), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(AppBorderRadius.md), + borderSide: BorderSide( + color: context.conduitTheme.buttonPrimary, + width: 1, + ), + ), + contentPadding: const EdgeInsets.symmetric( + horizontal: Spacing.md, + vertical: Spacing.md, + ), + ), + onChanged: _filterModels, + ), + ), + + const SizedBox(height: Spacing.sm), + + // Models list + Expanded( + child: Scrollbar( + controller: scrollController, + child: _filteredModels.isEmpty + ? Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Platform.isIOS + ? CupertinoIcons.search_circle + : Icons.search_off, + size: 48, + color: context.conduitTheme.iconSecondary, + ), + const SizedBox(height: Spacing.md), + Text( + 'No results', + style: TextStyle( + color: context.conduitTheme.textSecondary, + fontSize: AppTypography.bodyLarge, + ), + ), + ], + ), + ) + : ListView.builder( + controller: scrollController, + padding: EdgeInsets.zero, + itemCount: _filteredModels.length, + itemBuilder: (context, index) { + final model = _filteredModels[index]; + final isAutoSelect = model.id == 'auto-select'; + final isSelected = isAutoSelect + ? _selectedModelId == null || _selectedModelId == 'auto-select' + : _selectedModelId == model.id; + + return _buildModelListTile( + model: model, + isSelected: isSelected, + isAutoSelect: isAutoSelect, + onTap: () { + setState(() { + _selectedModelId = isAutoSelect ? 'auto-select' : model.id; + }); + }, + ); + }, + ), + ), + ), + ], + ), + ), + ), + ); + }, + ); + } + + bool _modelSupportsReasoning(Model model) { + final params = model.supportedParameters ?? const []; + return params.any((p) => p.toLowerCase().contains('reasoning')); + } + + Widget _buildModelListTile({ + required Model model, + required bool isSelected, + required bool isAutoSelect, + required VoidCallback onTap, + }) { + return PressableScale( + onTap: onTap, + borderRadius: BorderRadius.circular(AppBorderRadius.md), + child: Container( + margin: const EdgeInsets.only(bottom: Spacing.md), + decoration: BoxDecoration( + gradient: isSelected + ? LinearGradient( + colors: [ + context.conduitTheme.buttonPrimary.withValues(alpha: 0.2), + context.conduitTheme.buttonPrimary.withValues(alpha: 0.1), + ], + ) + : null, + color: isSelected + ? null + : context.conduitTheme.surfaceBackground.withValues(alpha: 0.05), + borderRadius: BorderRadius.circular(AppBorderRadius.md), + border: Border.all( + color: isSelected + ? context.conduitTheme.buttonPrimary.withValues(alpha: 0.5) + : context.conduitTheme.dividerColor, + width: BorderWidth.regular, + ), + boxShadow: isSelected ? ConduitShadows.card : null, + ), + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: Spacing.md, + vertical: Spacing.sm, + ), + child: Row( + children: [ + Container( + width: 32, + height: 32, + decoration: BoxDecoration( + 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), + color: context.conduitTheme.buttonPrimary, + size: 16, + ), + ), + const SizedBox(width: Spacing.md), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + isAutoSelect ? 'Auto-select' : model.name, + style: TextStyle( + color: context.conduitTheme.textPrimary, + fontWeight: FontWeight.w600, + fontSize: AppTypography.bodyMedium, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + if (isAutoSelect) ...[ + const SizedBox(height: Spacing.xs), + Text( + 'Let the app choose the best model', + style: TextStyle( + fontSize: AppTypography.bodySmall, + color: context.conduitTheme.textSecondary, + fontWeight: FontWeight.w400, + ), + ), + ] else ...[ + const SizedBox(height: Spacing.xs), + Row( + children: [ + if (model.isMultimodal) + _capabilityChip( + icon: Platform.isIOS + ? CupertinoIcons.photo + : Icons.image, + label: 'Multimodal', + ), + if (_modelSupportsReasoning(model)) + _capabilityChip( + icon: Platform.isIOS + ? CupertinoIcons.lightbulb + : Icons.psychology_alt, + label: 'Reasoning', + ), + ], + ), + ], + ], + ), + ), + const SizedBox(width: Spacing.md), + AnimatedOpacity( + opacity: isSelected ? 1 : 0.6, + duration: AnimationDuration.fast, + child: Container( + padding: const EdgeInsets.all(Spacing.xxs), + decoration: BoxDecoration( + color: isSelected + ? context.conduitTheme.buttonPrimary + : context.conduitTheme.surfaceBackground, + borderRadius: BorderRadius.circular(AppBorderRadius.md), + border: Border.all( + color: isSelected + ? context.conduitTheme.buttonPrimary.withValues(alpha: 0.6) + : context.conduitTheme.dividerColor, + ), + ), + child: Icon( + isSelected + ? (Platform.isIOS ? CupertinoIcons.check_mark : Icons.check) + : (Platform.isIOS ? CupertinoIcons.add : Icons.add), + color: isSelected + ? context.conduitTheme.textInverse + : context.conduitTheme.iconSecondary, + size: 14, + ), + ), + ), + ], + ), + ), + ), + ).animate().fadeIn(duration: AnimationDuration.microInteraction); + } +} diff --git a/lib/features/settings/views/accessibility_settings_page.dart b/lib/features/settings/views/accessibility_settings_page.dart deleted file mode 100644 index 686c860..0000000 --- a/lib/features/settings/views/accessibility_settings_page.dart +++ /dev/null @@ -1,278 +0,0 @@ -import 'package:flutter/material.dart'; -import '../../../shared/theme/theme_extensions.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; - -import '../../../shared/widgets/conduit_components.dart'; -import '../../../core/services/settings_service.dart'; -import '../../../core/services/enhanced_accessibility_service.dart'; -import '../../../core/services/platform_service.dart'; - -/// Accessibility settings page with WCAG 2.2 AA compliance controls -class AccessibilitySettingsPage extends ConsumerWidget { - const AccessibilitySettingsPage({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final settings = ref.watch(appSettingsProvider); - - return Scaffold( - backgroundColor: context.conduitTheme.surfaceBackground, - appBar: PlatformService.createPlatformAppBar( - title: 'Accessibility', - backgroundColor: context.conduitTheme.surfaceBackground, - foregroundColor: context.conduitTheme.textPrimary, - ), - body: SingleChildScrollView( - padding: const EdgeInsets.all(Spacing.md), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _buildSectionHeader(context, 'Motion & Animation'), - const SizedBox(height: Spacing.sm), - - // Reduce Motion Toggle - ConduitCard( - child: EnhancedAccessibilityService.createAccessibleSwitch( - value: settings.reduceMotion, - onChanged: (value) { - ref.read(appSettingsProvider.notifier).setReduceMotion(value); - EnhancedAccessibilityService.announceSuccess( - value - ? 'Reduced motion enabled' - : 'Reduced motion disabled', - ); - }, - label: 'Reduce Motion', - description: - 'Minimize animations and transitions for better focus and reduced vestibular disturbance', - ), - ), - - const SizedBox(height: Spacing.sm), - - // Animation Speed Slider - if (!settings.reduceMotion) ...[ - ConduitCard( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Animation Speed', - style: TextStyle( - color: context.conduitTheme.textPrimary, - fontSize: AppTypography.bodyLarge, - fontWeight: FontWeight.w500, - ), - ), - const SizedBox(height: Spacing.sm), - Text( - 'Adjust the speed of animations and transitions', - style: TextStyle( - color: context.conduitTheme.textSecondary, - fontSize: AppTypography.labelLarge, - ), - ), - const SizedBox(height: Spacing.md), - EnhancedAccessibilityService.createAccessibleSlider( - value: settings.animationSpeed, - onChanged: (value) { - ref - .read(appSettingsProvider.notifier) - .setAnimationSpeed(value); - }, - label: 'Animation speed', - min: 0.5, - max: 2.0, - divisions: 6, - valueFormatter: (value) { - if (value < 0.75) return 'Slow'; - if (value < 1.25) return 'Normal'; - return 'Fast'; - }, - ), - ], - ), - ), - const SizedBox(height: Spacing.sm), - ], - - const SizedBox(height: Spacing.lg), - _buildSectionHeader(context, 'Visual & Text'), - const SizedBox(height: Spacing.sm), - - // Large Text Toggle - ConduitCard( - child: EnhancedAccessibilityService.createAccessibleSwitch( - value: settings.largeText, - onChanged: (value) { - ref.read(appSettingsProvider.notifier).setLargeText(value); - EnhancedAccessibilityService.announceSuccess( - value ? 'Large text enabled' : 'Large text disabled', - ); - }, - label: 'Large Text', - description: - 'Increase text size throughout the app for better readability', - ), - ), - - const SizedBox(height: Spacing.sm), - - // High Contrast Toggle - ConduitCard( - child: EnhancedAccessibilityService.createAccessibleSwitch( - value: settings.highContrast, - onChanged: (value) { - ref.read(appSettingsProvider.notifier).setHighContrast(value); - EnhancedAccessibilityService.announceSuccess( - value ? 'High contrast enabled' : 'High contrast disabled', - ); - }, - label: 'High Contrast', - description: - 'Increase contrast between text and background colors', - ), - ), - - const SizedBox(height: Spacing.lg), - _buildSectionHeader(context, 'Interaction'), - const SizedBox(height: Spacing.sm), - - // Haptic Feedback Toggle - ConduitCard( - child: EnhancedAccessibilityService.createAccessibleSwitch( - value: settings.hapticFeedback, - onChanged: (value) { - ref - .read(appSettingsProvider.notifier) - .setHapticFeedback(value); - if (value) { - PlatformService.hapticFeedback(type: HapticType.success); - } - EnhancedAccessibilityService.announceSuccess( - value - ? 'Haptic feedback enabled' - : 'Haptic feedback disabled', - ); - }, - label: 'Haptic Feedback', - description: - 'Feel vibrations when interacting with buttons and controls', - ), - ), - - const SizedBox(height: Spacing.lg), - _buildSectionHeader(context, 'System Integration'), - const SizedBox(height: Spacing.sm), - - // System Settings Info Card - ConduitCard( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Icon( - Icons.info_outline, - color: context.conduitTheme.buttonPrimary, - size: IconSize.md, - ), - const SizedBox(width: Spacing.sm), - Text( - 'System Settings', - style: TextStyle( - color: context.conduitTheme.textPrimary, - fontSize: AppTypography.bodyLarge, - fontWeight: FontWeight.w500, - ), - ), - ], - ), - const SizedBox(height: Spacing.sm), - Text( - 'Conduit automatically respects your device\'s accessibility settings, including:', - style: TextStyle( - color: context.conduitTheme.textSecondary, - fontSize: AppTypography.labelLarge, - ), - ), - const SizedBox(height: Spacing.sm), - ...[ - '• Reduce Motion (iOS/Android)', - '• VoiceOver/TalkBack screen readers', - '• Dynamic Type/Font scale', - '• Color inversion and filters', - ].map( - (item) => Padding( - padding: const EdgeInsets.only(bottom: 4), - child: Text( - item, - style: TextStyle( - color: context.conduitTheme.textSecondary, - fontSize: AppTypography.labelLarge, - ), - ), - ), - ), - ], - ), - ), - - const SizedBox(height: Spacing.lg), - - // Reset to Defaults Button - ConduitButton( - text: 'Reset to Defaults', - onPressed: () => _showResetDialog(context, ref), - isSecondary: true, - width: double.infinity, - ), - - const SizedBox(height: Spacing.xl), - ], - ), - ), - ); - } - - Widget _buildSectionHeader(BuildContext context, String title) { - return EnhancedAccessibilityService.createAccessibleText( - title, - style: TextStyle( - color: context.conduitTheme.buttonPrimary, - fontSize: AppTypography.headlineSmall, - fontWeight: FontWeight.w600, - ), - isHeader: true, - ); - } - - Future _showResetDialog(BuildContext context, WidgetRef ref) async { - final confirmed = await PlatformService.showPlatformAlert( - context: context, - title: 'Reset Accessibility Settings', - content: - 'This will reset all accessibility preferences to their default values. Are you sure?', - confirmText: 'Reset', - cancelText: 'Cancel', - isDestructive: true, - ); - - if (confirmed == true) { - await ref.read(appSettingsProvider.notifier).resetToDefaults(); - EnhancedAccessibilityService.announceSuccess( - 'Accessibility settings reset to defaults', - ); - - if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: const Text('Accessibility settings reset to defaults'), - backgroundColor: context.conduitTheme.buttonPrimary, - behavior: SnackBarBehavior.floating, - ), - ); - } - } - } -} diff --git a/lib/features/settings/views/searchable_settings_page.dart b/lib/features/settings/views/searchable_settings_page.dart deleted file mode 100644 index 6f545cc..0000000 --- a/lib/features/settings/views/searchable_settings_page.dart +++ /dev/null @@ -1,810 +0,0 @@ -import 'package:flutter/material.dart'; -import '../../../shared/theme/theme_extensions.dart'; -import 'package:flutter/cupertino.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'dart:io' show Platform; -import '../../../core/widgets/error_boundary.dart'; -import '../../../core/services/navigation_service.dart'; -import '../../../shared/widgets/themed_dialogs.dart'; -import '../../../core/services/focus_management_service.dart'; -import '../../../shared/widgets/improved_loading_states.dart'; -import '../../../shared/widgets/conduit_components.dart'; -import '../../../core/models/user_settings.dart'; -import '../../../core/providers/app_providers.dart'; -import '../../../shared/utils/platform_utils.dart'; - -enum ThemeVariant { conduit } - -// Settings search provider -final settingsSearchQueryProvider = StateProvider((ref) => ''); - -// Setting item model -class SettingItem { - final String id; - final String title; - final String? subtitle; - final IconData icon; - final String category; - final List searchTerms; - final VoidCallback? onTap; - final Widget? trailing; - - SettingItem({ - required this.id, - required this.title, - this.subtitle, - required this.icon, - required this.category, - required this.searchTerms, - this.onTap, - this.trailing, - }); - - bool matchesSearch(String query) { - final lowerQuery = query.toLowerCase(); - return title.toLowerCase().contains(lowerQuery) || - (subtitle?.toLowerCase().contains(lowerQuery) ?? false) || - category.toLowerCase().contains(lowerQuery) || - searchTerms.any((term) => term.toLowerCase().contains(lowerQuery)); - } -} - -class SearchableSettingsPage extends ConsumerStatefulWidget { - const SearchableSettingsPage({super.key}); - - @override - ConsumerState createState() => - _SearchableSettingsPageState(); -} - -class _SearchableSettingsPageState - extends ConsumerState { - final TextEditingController _searchController = TextEditingController(); - late FocusNode _searchFocusNode; - bool _isSearching = false; - - @override - void initState() { - super.initState(); - _searchFocusNode = FocusManagementService.registerFocusNode( - 'settings_search', - debugLabel: 'Settings Search Field', - ); - } - - @override - void dispose() { - _searchController.dispose(); - FocusManagementService.disposeFocusNode('settings_search'); - super.dispose(); - } - - List _buildSettingItems(BuildContext context, WidgetRef ref) { - final themeMode = ref.watch(themeModeProvider); - // Single Conduit theme variant in this refactor; kept provider for future use - final userSettingsAsync = ref.watch(userSettingsProvider); - final userSettings = userSettingsAsync.when( - data: (data) => data, - loading: () => null, - error: (_, _) => null, - ); - - return [ - // Profile & Account - SettingItem( - id: 'profile', - title: 'Profile', - subtitle: 'Manage your account details', - icon: Platform.isIOS - ? CupertinoIcons.person_circle - : Icons.account_circle, - category: 'Profile & Account', - searchTerms: ['account', 'user', 'name', 'email', 'avatar'], - onTap: () => _navigateToProfile(context), - ), - SettingItem( - id: 'server', - title: 'Server Connection', - subtitle: 'Manage Open WebUI servers', - icon: Platform.isIOS ? CupertinoIcons.cloud : Icons.cloud, - category: 'Profile & Account', - searchTerms: ['server', 'connection', 'api', 'host', 'url'], - onTap: () => _navigateToServerSettings(context), - ), - SettingItem( - id: 'sign-out', - title: 'Sign Out', - subtitle: 'Sign out of your account', - icon: Platform.isIOS ? CupertinoIcons.square_arrow_right : Icons.logout, - category: 'Profile & Account', - searchTerms: ['logout', 'signout', 'exit'], - onTap: () => _handleSignOut(context, ref), - ), - - // Appearance - SettingItem( - id: 'theme', - title: 'Theme', - subtitle: 'Choose light or dark theme', - icon: Platform.isIOS ? CupertinoIcons.moon_circle : Icons.dark_mode, - category: 'Appearance', - searchTerms: ['dark', 'light', 'mode', 'appearance', 'color'], - trailing: _buildThemeSelector(ref, themeMode), - ), - // Removed variant switching; Conduit brand theme is the single source of truth - SettingItem( - id: 'text-size', - title: 'Text Size', - subtitle: 'Adjust font size for better readability', - icon: Platform.isIOS - ? CupertinoIcons.textformat_size - : Icons.text_fields, - category: 'Appearance', - searchTerms: ['font', 'size', 'text', 'readability', 'accessibility'], - onTap: () => _showTextSizeDialog(context), - ), - - // Chat & AI - SettingItem( - id: 'stream-responses', - title: 'Stream Responses', - subtitle: 'See responses as they\'re generated', - icon: Platform.isIOS ? CupertinoIcons.bolt : Icons.flash_on, - category: 'Chat & AI', - searchTerms: ['stream', 'real-time', 'live', 'responses'], - trailing: PlatformUtils.createSwitch( - value: userSettings?.streamResponses ?? true, - onChanged: (value) => _updateSetting(ref, 'streamResponses', value), - ), - ), - SettingItem( - id: 'save-conversations', - title: 'Save Conversations', - subtitle: 'Keep chat history between sessions', - icon: Platform.isIOS ? CupertinoIcons.archivebox : Icons.save, - category: 'Chat & AI', - searchTerms: ['save', 'history', 'conversations', 'chat', 'archive'], - trailing: PlatformUtils.createSwitch( - value: userSettings?.saveConversations ?? true, - onChanged: (value) => _updateSetting(ref, 'saveConversations', value), - ), - ), - SettingItem( - id: 'web-search', - title: 'Web Search', - subtitle: 'Allow AI to search the web for information', - icon: Platform.isIOS ? CupertinoIcons.globe : Icons.public, - category: 'Chat & AI', - searchTerms: ['web', 'search', 'internet', 'browse', 'online'], - trailing: Consumer( - builder: (context, ref, child) { - final settings = ref.watch(userSettingsProvider); - return settings.when( - data: (userSettings) => PlatformUtils.createSwitch( - value: userSettings.webSearchEnabled, - onChanged: (value) => - _updateSetting(ref, 'webSearchEnabled', value), - ), - loading: () => - const ImprovedLoadingState(message: 'Loading setting...'), - error: (error, stackTrace) => PlatformUtils.createSwitch( - value: false, - onChanged: (value) => - _updateSetting(ref, 'webSearchEnabled', value), - ), - ); - }, - ), - ), - SettingItem( - id: 'model-selection', - title: 'Default Model', - subtitle: 'Choose your preferred AI model', - icon: Platform.isIOS ? CupertinoIcons.cube : Icons.psychology, - category: 'Chat & AI', - searchTerms: ['model', 'ai', 'gpt', 'conduit', 'llm'], - onTap: () => _showModelSelector(context), - ), - - // Privacy & Security - SettingItem( - id: 'clear-history', - title: 'Clear Chat History', - subtitle: 'Delete all conversations', - icon: Platform.isIOS ? CupertinoIcons.trash : Icons.delete_outline, - category: 'Privacy & Security', - searchTerms: ['clear', 'delete', 'history', 'privacy', 'remove'], - onTap: () => _showClearHistoryDialog(context, ref), - ), - SettingItem( - id: 'export-data', - title: 'Export Data', - subtitle: 'Download your conversations', - icon: Platform.isIOS - ? CupertinoIcons.square_arrow_down - : Icons.download, - category: 'Privacy & Security', - searchTerms: ['export', 'download', 'backup', 'data'], - onTap: () => _handleExportData(context), - ), - - // Accessibility - SettingItem( - id: 'reduce-motion', - title: 'Reduce Motion', - subtitle: 'Minimize animations', - icon: Platform.isIOS ? CupertinoIcons.slowmo : Icons.animation, - category: 'Accessibility', - searchTerms: ['motion', 'animation', 'reduce', 'accessibility'], - trailing: Consumer( - builder: (context, ref, child) { - final settings = ref.watch(userSettingsProvider); - return settings.when( - data: (userSettings) => PlatformUtils.createSwitch( - value: userSettings.reduceMotion, - onChanged: (value) => - _updateSetting(ref, 'reduceMotion', value), - ), - loading: () => - const ImprovedLoadingState(message: 'Loading setting...'), - error: (error, stackTrace) => PlatformUtils.createSwitch( - value: false, - onChanged: (value) => - _updateSetting(ref, 'reduceMotion', value), - ), - ); - }, - ), - ), - SettingItem( - id: 'haptic-feedback', - title: 'Haptic Feedback', - subtitle: 'Vibration feedback for actions', - icon: Platform.isIOS ? CupertinoIcons.hand_draw : Icons.vibration, - category: 'Accessibility', - searchTerms: ['haptic', 'vibration', 'feedback', 'touch'], - trailing: Consumer( - builder: (context, ref, child) { - final settings = ref.watch(userSettingsProvider); - return settings.when( - data: (userSettings) => PlatformUtils.createSwitch( - value: userSettings.hapticFeedback, - onChanged: (value) => - _updateSetting(ref, 'hapticFeedback', value), - ), - loading: () => - const ImprovedLoadingState(message: 'Loading setting...'), - error: (error, stackTrace) => PlatformUtils.createSwitch( - value: true, - onChanged: (value) => - _updateSetting(ref, 'hapticFeedback', value), - ), - ); - }, - ), - ), - - // About - SettingItem( - id: 'version', - title: 'App Version', - subtitle: 'Conduit v1.0.0', - icon: Platform.isIOS ? CupertinoIcons.info_circle : Icons.info_outline, - category: 'About', - searchTerms: ['version', 'about', 'info', 'conduit'], - onTap: () => _showAboutDialog(context), - ), - SettingItem( - id: 'help', - title: 'Help & Support', - subtitle: 'Get assistance and report issues', - icon: Platform.isIOS - ? CupertinoIcons.question_circle - : Icons.help_outline, - category: 'About', - searchTerms: ['help', 'support', 'assistance', 'contact'], - onTap: () => _navigateToHelp(context), - ), - ]; - } - - List _getFilteredSettings(BuildContext context, WidgetRef ref) { - final searchQuery = ref.watch(settingsSearchQueryProvider); - final allSettings = _buildSettingItems(context, ref); - - if (searchQuery.isEmpty) { - return allSettings; - } - - return allSettings - .where((item) => item.matchesSearch(searchQuery)) - .toList(); - } - - Map> _groupSettingsByCategory( - List settings, - ) { - final grouped = >{}; - - for (final setting in settings) { - grouped.putIfAbsent(setting.category, () => []).add(setting); - } - - return grouped; - } - - @override - Widget build(BuildContext context) { - final filteredSettings = _getFilteredSettings(context, ref); - final groupedSettings = _groupSettingsByCategory(filteredSettings); - final categories = groupedSettings.keys.toList()..sort(); - - return ErrorBoundary( - child: Scaffold( - backgroundColor: context.conduitTheme.surfaceBackground, - appBar: AppBar( - backgroundColor: Colors.transparent, - elevation: Elevation.none, - title: _isSearching - ? _buildSearchBar() - : Text( - 'Settings', - style: TextStyle( - color: context.conduitTheme.textPrimary, - fontSize: AppTypography.headlineMedium, - fontWeight: FontWeight.w600, - ), - ), - leading: ConduitIconButton( - icon: Platform.isIOS ? CupertinoIcons.back : Icons.arrow_back, - onPressed: () { - if (_isSearching) { - setState(() { - _isSearching = false; - _searchController.clear(); - ref.read(settingsSearchQueryProvider.notifier).state = ''; - }); - } else { - NavigationService.goBack(); - } - }, - ), - actions: [ - if (!_isSearching) - ConduitIconButton( - icon: Platform.isIOS ? CupertinoIcons.search : Icons.search, - onPressed: () { - setState(() { - _isSearching = true; - }); - _searchFocusNode.requestFocus(); - }, - ), - const SizedBox(width: Spacing.sm), - ], - ), - body: SafeArea( - top: false, - child: filteredSettings.isEmpty - ? _buildEmptySearchResults() - : ListView.builder( - padding: const EdgeInsets.symmetric(vertical: 8), - itemCount: categories.length, - itemBuilder: (context, index) { - final category = categories[index]; - final items = groupedSettings[category]!; - - return _buildCategorySection(category, items); - }, - ), - ), - ), // Added closing parenthesis for ErrorBoundary - ); - } - - Widget _buildSearchBar() { - return TextField( - controller: _searchController, - focusNode: _searchFocusNode, - style: TextStyle( - color: context.conduitTheme.textPrimary, - fontSize: AppTypography.bodyLarge, - ), - decoration: InputDecoration( - hintText: 'Search settings...', - hintStyle: TextStyle( - color: context.conduitTheme.inputPlaceholder, - fontSize: AppTypography.bodyLarge, - ), - border: InputBorder.none, - enabledBorder: InputBorder.none, - focusedBorder: InputBorder.none, - ), - onChanged: (value) { - ref.read(settingsSearchQueryProvider.notifier).state = value; - }, - ); - } - - Widget _buildEmptySearchResults() { - return ImprovedEmptyState( - title: 'No settings found', - subtitle: 'Try a different search term', - icon: Platform.isIOS ? CupertinoIcons.search : Icons.search_off, - showAnimation: true, - ); - } - - Widget _buildCategorySection(String category, List items) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.fromLTRB( - Spacing.md, - Spacing.md, - Spacing.md, - Spacing.sm, - ), - child: Text( - category, - style: TextStyle( - color: context.conduitTheme.textSecondary, - fontSize: AppTypography.bodySmall, - fontWeight: FontWeight.w600, - letterSpacing: 0.5, - ), - ), - ), - Container( - margin: const EdgeInsets.symmetric(horizontal: 16), - decoration: BoxDecoration( - color: context.conduitTheme.surfaceBackground, - borderRadius: BorderRadius.circular(AppBorderRadius.md), - border: Border.all( - color: context.conduitTheme.dividerColor, - width: 1, - ), - ), - child: Column( - children: items.asMap().entries.map((entry) { - final index = entry.key; - final item = entry.value; - final isLast = index == items.length - 1; - - return Column( - children: [ - _buildSettingTile(item), - if (!isLast) - Divider( - height: 1, - color: context.conduitTheme.dividerColor, - indent: 56, - ), - ], - ); - }).toList(), - ), - ), - ], - ); - } - - Widget _buildSettingTile(SettingItem item) { - final searchQuery = ref.watch(settingsSearchQueryProvider); - - return Material( - color: Colors.transparent, - child: InkWell( - onTap: item.onTap, - borderRadius: BorderRadius.circular(AppBorderRadius.md), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), - child: Row( - children: [ - Container( - width: 40, - height: 40, - decoration: BoxDecoration( - color: context.conduitTheme.surfaceBackground, - borderRadius: BorderRadius.circular(AppBorderRadius.sm), - ), - child: Icon( - item.icon, - color: context.conduitTheme.iconSecondary, - size: IconSize.md, - ), - ), - const SizedBox(width: Spacing.md), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _highlightSearchText(item.title, searchQuery), - if (item.subtitle != null) ...[ - const SizedBox(height: Spacing.xxs), - _highlightSearchText( - item.subtitle!, - searchQuery, - style: TextStyle( - color: context.conduitTheme.textSecondary, - fontSize: AppTypography.bodySmall, - ), - ), - ], - ], - ), - ), - if (item.trailing != null) ...[ - const SizedBox(width: Spacing.sm), - item.trailing!, - ] else if (item.onTap != null) - Icon( - Platform.isIOS - ? CupertinoIcons.chevron_forward - : Icons.chevron_right, - color: context.conduitTheme.iconSecondary, - size: IconSize.md, - ), - ], - ), - ), - ), - ); - } - - Widget _highlightSearchText(String text, String query, {TextStyle? style}) { - if (query.isEmpty) { - return Text( - text, - style: - style ?? - TextStyle( - color: context.conduitTheme.textPrimary, - fontSize: AppTypography.bodyLarge, - fontWeight: FontWeight.w500, - ), - ); - } - - final lowerText = text.toLowerCase(); - final lowerQuery = query.toLowerCase(); - final index = lowerText.indexOf(lowerQuery); - - if (index == -1) { - return Text(text, style: style); - } - - final before = text.substring(0, index); - final match = text.substring(index, index + query.length); - final after = text.substring(index + query.length); - - return RichText( - text: TextSpan( - style: - style ?? - TextStyle( - color: context.conduitTheme.textPrimary, - fontSize: AppTypography.bodyLarge, - fontWeight: FontWeight.w500, - ), - children: [ - TextSpan(text: before), - TextSpan( - text: match, - style: TextStyle( - backgroundColor: context.conduitTheme.buttonPrimary.withValues( - alpha: 0.3, - ), - fontWeight: FontWeight.w600, - ), - ), - TextSpan(text: after), - ], - ), - ); - } - - Widget _buildThemeSelector(WidgetRef ref, ThemeMode themeMode) { - return CupertinoSlidingSegmentedControl( - groupValue: themeMode, - children: const { - ThemeMode.light: Padding( - padding: EdgeInsets.symmetric(horizontal: 8), - child: Text( - 'Light', - style: TextStyle(fontSize: AppTypography.bodySmall), - ), - ), - ThemeMode.dark: Padding( - padding: EdgeInsets.symmetric(horizontal: 8), - child: Text( - 'Dark', - style: TextStyle(fontSize: AppTypography.bodySmall), - ), - ), - ThemeMode.system: Padding( - padding: EdgeInsets.symmetric(horizontal: 8), - child: Text( - 'Auto', - style: TextStyle(fontSize: AppTypography.bodySmall), - ), - ), - }, - onValueChanged: (value) { - if (value != null) { - ref.read(themeModeProvider.notifier).setTheme(value); - } - }, - ); - } - - // Theme variant state removed; single Conduit theme in use - - void _updateSetting(WidgetRef ref, String key, dynamic value) async { - try { - final currentSettings = await ref.read(userSettingsProvider.future); - - // Create updated settings based on the key - UserSettings updatedSettings; - switch (key) { - case 'webSearchEnabled': - updatedSettings = currentSettings.copyWith( - webSearchEnabled: value as bool, - ); - break; - case 'reduceMotion': - updatedSettings = currentSettings.copyWith( - reduceMotion: value as bool, - ); - break; - case 'hapticFeedback': - updatedSettings = currentSettings.copyWith( - hapticFeedback: value as bool, - ); - break; - case 'streamResponses': - updatedSettings = currentSettings.copyWith( - streamResponses: value as bool, - ); - break; - case 'saveConversations': - updatedSettings = currentSettings.copyWith( - saveConversations: value as bool, - ); - break; - case 'showReadReceipts': - updatedSettings = currentSettings.copyWith( - showReadReceipts: value as bool, - ); - break; - case 'enableNotifications': - updatedSettings = currentSettings.copyWith( - enableNotifications: value as bool, - ); - break; - case 'enableSounds': - updatedSettings = currentSettings.copyWith( - enableSounds: value as bool, - ); - break; - case 'shareUsageData': - updatedSettings = currentSettings.copyWith( - shareUsageData: value as bool, - ); - break; - case 'temperature': - updatedSettings = currentSettings.copyWith( - temperature: value as double, - ); - break; - case 'maxTokens': - updatedSettings = currentSettings.copyWith(maxTokens: value as int); - break; - case 'fontSize': - updatedSettings = currentSettings.copyWith(fontSize: value as double); - break; - case 'theme': - updatedSettings = currentSettings.copyWith(theme: value as String); - break; - case 'density': - updatedSettings = currentSettings.copyWith(density: value as String); - break; - case 'language': - updatedSettings = currentSettings.copyWith(language: value as String); - break; - default: - // Handle custom settings - final customSettings = Map.from( - currentSettings.customSettings, - ); - customSettings[key] = value; - updatedSettings = currentSettings.copyWith( - customSettings: customSettings, - ); - } - - // Update settings on server - final api = ref.read(apiServiceProvider); - if (api != null) { - await api.updateUserSettings(updatedSettings.toJson()); - - // Invalidate the provider to refresh the UI - ref.invalidate(userSettingsProvider); - - // Show success message - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('Setting updated'), - duration: const Duration(seconds: 2), - ), - ); - } - } - } catch (e) { - // Show error message - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('Failed to update setting: $e'), - backgroundColor: context.conduitTheme.error, - ), - ); - } - } - } - - void _navigateToProfile(BuildContext context) { - // TODO: Navigate to profile page - } - - void _navigateToServerSettings(BuildContext context) { - NavigationService.navigateTo('/server-connection'); - } - - void _handleSignOut(BuildContext context, WidgetRef ref) { - // ignore: unawaited_futures - ThemedDialogs.confirm( - context, - title: 'Sign Out', - message: 'Are you sure you want to sign out?', - confirmText: 'Sign Out', - ).then((confirmed) { - if (confirmed) { - // TODO: Implement proper logout functionality when auth service is available - // ref.read(authServiceProvider.notifier).logout(); - NavigationService.navigateTo('/login', clearStack: true); - } - }); - } - - void _showTextSizeDialog(BuildContext context) { - // TODO: Implement text size adjustment dialog - } - - void _showModelSelector(BuildContext context) { - // TODO: Implement model selection dialog - } - - void _showClearHistoryDialog(BuildContext context, WidgetRef ref) { - // TODO: Implement clear history dialog - } - - void _handleExportData(BuildContext context) { - // TODO: Implement data export - } - - void _showAboutDialog(BuildContext context) { - showAboutDialog( - context: context, - applicationName: 'Conduit', - applicationVersion: '1.0.0', - applicationLegalese: '© 2024 Conduit Team', - ); - } - - void _navigateToHelp(BuildContext context) { - // TODO: Navigate to help page - } -}