feat: separate default model for the app
This commit is contained in:
@@ -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<String, dynamic> customSettings,
|
||||
}) = _UserSettings;
|
||||
|
||||
@@ -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<Conversation, String>((
|
||||
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<Model?>((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<Model?>((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
|
||||
|
||||
@@ -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<void> navigateToSettings() {
|
||||
return navigateTo(Routes.settings);
|
||||
}
|
||||
|
||||
|
||||
/// Navigate to profile
|
||||
static Future<void> 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';
|
||||
|
||||
@@ -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<bool> getReduceMotion() async {
|
||||
@@ -84,6 +85,22 @@ class SettingsService {
|
||||
await prefs.setBool(_darkModeKey, value);
|
||||
}
|
||||
|
||||
/// Get default model preference
|
||||
static Future<String?> getDefaultModel() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
return prefs.getString(_defaultModelKey);
|
||||
}
|
||||
|
||||
/// Set default model preference
|
||||
static Future<void> 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<AppSettings> 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<AppSettings> {
|
||||
await SettingsService.setDarkMode(value);
|
||||
}
|
||||
|
||||
Future<void> setDefaultModel(String? modelId) async {
|
||||
state = state.copyWith(defaultModel: modelId);
|
||||
await SettingsService.setDefaultModel(modelId);
|
||||
}
|
||||
|
||||
Future<void> resetToDefaults() async {
|
||||
const defaultSettings = AppSettings();
|
||||
await SettingsService.saveSettings(defaultSettings);
|
||||
|
||||
@@ -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<void> _showModelSelector(BuildContext context, WidgetRef ref, List<Model> models) async {
|
||||
final result = await showModalBottomSheet<String?>(
|
||||
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<Model> 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<Model> _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<Model> 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<void> _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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<String>((ref) => '');
|
||||
|
||||
// Setting item model
|
||||
class SettingItem {
|
||||
final String id;
|
||||
final String title;
|
||||
final String? subtitle;
|
||||
final IconData icon;
|
||||
final String category;
|
||||
final List<String> 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<SearchableSettingsPage> createState() =>
|
||||
_SearchableSettingsPageState();
|
||||
}
|
||||
|
||||
class _SearchableSettingsPageState
|
||||
extends ConsumerState<SearchableSettingsPage> {
|
||||
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<SettingItem> _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<SettingItem> _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<String, List<SettingItem>> _groupSettingsByCategory(
|
||||
List<SettingItem> settings,
|
||||
) {
|
||||
final grouped = <String, List<SettingItem>>{};
|
||||
|
||||
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<SettingItem> 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<ThemeMode>(
|
||||
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<String, dynamic>.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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user