2025-08-10 01:20:45 +05:30
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
2025-09-30 15:01:47 +05:30
|
|
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
2025-10-01 16:55:44 +05:30
|
|
|
import 'package:hive_ce/hive.dart';
|
|
|
|
|
import '../persistence/hive_boxes.dart';
|
|
|
|
|
import '../persistence/persistence_keys.dart';
|
2025-08-10 01:20:45 +05:30
|
|
|
import 'animation_service.dart';
|
|
|
|
|
|
2025-09-30 15:01:47 +05:30
|
|
|
part 'settings_service.g.dart';
|
|
|
|
|
|
2025-11-02 19:02:37 +05:30
|
|
|
/// Speech-to-text preference selection.
|
|
|
|
|
enum SttPreference { auto, deviceOnly, serverOnly }
|
|
|
|
|
|
2025-10-23 16:31:15 +05:30
|
|
|
/// TTS engine selection
|
2025-11-02 21:31:13 +05:30
|
|
|
enum TtsEngine { auto, device, server }
|
2025-10-23 16:31:15 +05:30
|
|
|
|
2025-08-10 01:20:45 +05:30
|
|
|
/// Service for managing app-wide settings including accessibility preferences
|
|
|
|
|
class SettingsService {
|
2025-10-01 16:55:44 +05:30
|
|
|
static const String _reduceMotionKey = PreferenceKeys.reduceMotion;
|
|
|
|
|
static const String _animationSpeedKey = PreferenceKeys.animationSpeed;
|
|
|
|
|
static const String _hapticFeedbackKey = PreferenceKeys.hapticFeedback;
|
|
|
|
|
static const String _highContrastKey = PreferenceKeys.highContrast;
|
|
|
|
|
static const String _largeTextKey = PreferenceKeys.largeText;
|
|
|
|
|
static const String _darkModeKey = PreferenceKeys.darkMode;
|
|
|
|
|
static const String _defaultModelKey = PreferenceKeys.defaultModel;
|
2025-08-22 13:54:58 +05:30
|
|
|
// Voice input settings
|
2025-10-01 16:55:44 +05:30
|
|
|
static const String _voiceLocaleKey = PreferenceKeys.voiceLocaleId;
|
|
|
|
|
static const String _voiceHoldToTalkKey = PreferenceKeys.voiceHoldToTalk;
|
|
|
|
|
static const String _voiceAutoSendKey = PreferenceKeys.voiceAutoSendFinal;
|
2025-09-07 11:13:05 +05:30
|
|
|
// Realtime transport preference
|
2025-09-21 22:31:44 +05:30
|
|
|
static const String _socketTransportModeKey =
|
2025-10-30 22:32:59 +05:30
|
|
|
PreferenceKeys.socketTransportMode; // 'polling' or 'ws'
|
2025-09-07 14:40:20 +05:30
|
|
|
// Quick pill visibility selections (max 2)
|
2025-10-01 16:55:44 +05:30
|
|
|
static const String _quickPillsKey = PreferenceKeys
|
|
|
|
|
.quickPills; // StringList of identifiers e.g. ['web','image','tools']
|
2025-09-08 01:05:48 +05:30
|
|
|
// Chat input behavior
|
2025-10-01 16:55:44 +05:30
|
|
|
static const String _sendOnEnterKey = PreferenceKeys.sendOnEnterKey;
|
|
|
|
|
static Box<dynamic> _preferencesBox() =>
|
|
|
|
|
Hive.box<dynamic>(HiveBoxNames.preferences);
|
2025-08-10 01:20:45 +05:30
|
|
|
|
|
|
|
|
/// Get reduced motion preference
|
2025-10-01 16:55:44 +05:30
|
|
|
static Future<bool> getReduceMotion() {
|
|
|
|
|
final value = _preferencesBox().get(_reduceMotionKey) as bool?;
|
|
|
|
|
return Future.value(value ?? false);
|
2025-08-10 01:20:45 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Set reduced motion preference
|
2025-10-01 16:55:44 +05:30
|
|
|
static Future<void> setReduceMotion(bool value) {
|
|
|
|
|
return _preferencesBox().put(_reduceMotionKey, value);
|
2025-08-10 01:20:45 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Get animation speed multiplier (0.5 - 2.0)
|
2025-10-01 16:55:44 +05:30
|
|
|
static Future<double> getAnimationSpeed() {
|
|
|
|
|
final value = _preferencesBox().get(_animationSpeedKey) as num?;
|
|
|
|
|
return Future.value((value?.toDouble() ?? 1.0).clamp(0.5, 2.0));
|
2025-08-10 01:20:45 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Set animation speed multiplier
|
2025-10-01 16:55:44 +05:30
|
|
|
static Future<void> setAnimationSpeed(double value) {
|
|
|
|
|
final sanitized = value.clamp(0.5, 2.0).toDouble();
|
|
|
|
|
return _preferencesBox().put(_animationSpeedKey, sanitized);
|
2025-08-10 01:20:45 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Get haptic feedback preference
|
2025-10-01 16:55:44 +05:30
|
|
|
static Future<bool> getHapticFeedback() {
|
|
|
|
|
final value = _preferencesBox().get(_hapticFeedbackKey) as bool?;
|
|
|
|
|
return Future.value(value ?? true);
|
2025-08-10 01:20:45 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Set haptic feedback preference
|
2025-10-01 16:55:44 +05:30
|
|
|
static Future<void> setHapticFeedback(bool value) {
|
|
|
|
|
return _preferencesBox().put(_hapticFeedbackKey, value);
|
2025-08-10 01:20:45 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Get high contrast preference
|
2025-10-01 16:55:44 +05:30
|
|
|
static Future<bool> getHighContrast() {
|
|
|
|
|
final value = _preferencesBox().get(_highContrastKey) as bool?;
|
|
|
|
|
return Future.value(value ?? false);
|
2025-08-10 01:20:45 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Set high contrast preference
|
2025-10-01 16:55:44 +05:30
|
|
|
static Future<void> setHighContrast(bool value) {
|
|
|
|
|
return _preferencesBox().put(_highContrastKey, value);
|
2025-08-10 01:20:45 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Get large text preference
|
2025-10-01 16:55:44 +05:30
|
|
|
static Future<bool> getLargeText() {
|
|
|
|
|
final value = _preferencesBox().get(_largeTextKey) as bool?;
|
|
|
|
|
return Future.value(value ?? false);
|
2025-08-10 01:20:45 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Set large text preference
|
2025-10-01 16:55:44 +05:30
|
|
|
static Future<void> setLargeText(bool value) {
|
|
|
|
|
return _preferencesBox().put(_largeTextKey, value);
|
2025-08-10 01:20:45 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Get dark mode preference
|
2025-10-01 16:55:44 +05:30
|
|
|
static Future<bool> getDarkMode() {
|
|
|
|
|
final value = _preferencesBox().get(_darkModeKey) as bool?;
|
|
|
|
|
return Future.value(value ?? true);
|
2025-08-10 01:20:45 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Set dark mode preference
|
2025-10-01 16:55:44 +05:30
|
|
|
static Future<void> setDarkMode(bool value) {
|
|
|
|
|
return _preferencesBox().put(_darkModeKey, value);
|
2025-08-10 01:20:45 +05:30
|
|
|
}
|
|
|
|
|
|
2025-08-17 17:01:06 +05:30
|
|
|
/// Get default model preference
|
2025-10-01 16:55:44 +05:30
|
|
|
static Future<String?> getDefaultModel() {
|
|
|
|
|
final value = _preferencesBox().get(_defaultModelKey) as String?;
|
|
|
|
|
return Future.value(value);
|
2025-08-17 17:01:06 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Set default model preference
|
2025-10-01 16:55:44 +05:30
|
|
|
static Future<void> setDefaultModel(String? modelId) {
|
|
|
|
|
final box = _preferencesBox();
|
2025-08-17 17:01:06 +05:30
|
|
|
if (modelId != null) {
|
2025-10-01 16:55:44 +05:30
|
|
|
return box.put(_defaultModelKey, modelId);
|
2025-08-17 17:01:06 +05:30
|
|
|
}
|
2025-10-01 16:55:44 +05:30
|
|
|
return box.delete(_defaultModelKey);
|
2025-08-17 17:01:06 +05:30
|
|
|
}
|
|
|
|
|
|
2025-08-10 01:20:45 +05:30
|
|
|
/// Load all settings
|
2025-10-01 16:55:44 +05:30
|
|
|
static Future<AppSettings> loadSettings() {
|
|
|
|
|
final box = _preferencesBox();
|
|
|
|
|
return Future.value(
|
|
|
|
|
AppSettings(
|
|
|
|
|
reduceMotion: (box.get(_reduceMotionKey) as bool?) ?? false,
|
|
|
|
|
animationSpeed:
|
|
|
|
|
(box.get(_animationSpeedKey) as num?)?.toDouble() ?? 1.0,
|
|
|
|
|
hapticFeedback: (box.get(_hapticFeedbackKey) as bool?) ?? true,
|
|
|
|
|
highContrast: (box.get(_highContrastKey) as bool?) ?? false,
|
|
|
|
|
largeText: (box.get(_largeTextKey) as bool?) ?? false,
|
|
|
|
|
darkMode: (box.get(_darkModeKey) as bool?) ?? true,
|
|
|
|
|
defaultModel: box.get(_defaultModelKey) as String?,
|
|
|
|
|
voiceLocaleId: box.get(_voiceLocaleKey) as String?,
|
|
|
|
|
voiceHoldToTalk: (box.get(_voiceHoldToTalkKey) as bool?) ?? false,
|
|
|
|
|
voiceAutoSendFinal: (box.get(_voiceAutoSendKey) as bool?) ?? false,
|
|
|
|
|
socketTransportMode:
|
|
|
|
|
box.get(_socketTransportModeKey, defaultValue: 'ws') as String,
|
|
|
|
|
quickPills: List<String>.from(
|
|
|
|
|
(box.get(_quickPillsKey) as List<dynamic>?) ?? const <String>[],
|
|
|
|
|
),
|
|
|
|
|
sendOnEnter: (box.get(_sendOnEnterKey) as bool?) ?? false,
|
2025-10-17 14:40:44 +05:30
|
|
|
ttsVoice: box.get(PreferenceKeys.ttsVoice) as String?,
|
|
|
|
|
ttsSpeechRate:
|
|
|
|
|
(box.get(PreferenceKeys.ttsSpeechRate) as num?)?.toDouble() ?? 0.5,
|
|
|
|
|
ttsPitch: (box.get(PreferenceKeys.ttsPitch) as num?)?.toDouble() ?? 1.0,
|
|
|
|
|
ttsVolume:
|
|
|
|
|
(box.get(PreferenceKeys.ttsVolume) as num?)?.toDouble() ?? 1.0,
|
2025-10-23 16:31:15 +05:30
|
|
|
ttsEngine: _parseTtsEngine(
|
|
|
|
|
box.get(PreferenceKeys.ttsEngine) as String?,
|
|
|
|
|
),
|
|
|
|
|
ttsServerVoiceId: box.get(PreferenceKeys.ttsServerVoiceId) as String?,
|
|
|
|
|
ttsServerVoiceName:
|
|
|
|
|
box.get(PreferenceKeys.ttsServerVoiceName) as String?,
|
2025-11-02 19:02:37 +05:30
|
|
|
sttPreference: _parseSttPreference(
|
|
|
|
|
box.get(PreferenceKeys.voiceSttPreference) as String?,
|
|
|
|
|
),
|
2025-10-01 16:55:44 +05:30
|
|
|
),
|
2025-08-10 01:20:45 +05:30
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Save all settings
|
|
|
|
|
static Future<void> saveSettings(AppSettings settings) async {
|
2025-10-01 16:55:44 +05:30
|
|
|
final box = _preferencesBox();
|
|
|
|
|
final updates = <String, Object?>{
|
|
|
|
|
_reduceMotionKey: settings.reduceMotion,
|
|
|
|
|
_animationSpeedKey: settings.animationSpeed,
|
|
|
|
|
_hapticFeedbackKey: settings.hapticFeedback,
|
|
|
|
|
_highContrastKey: settings.highContrast,
|
|
|
|
|
_largeTextKey: settings.largeText,
|
|
|
|
|
_darkModeKey: settings.darkMode,
|
|
|
|
|
_voiceHoldToTalkKey: settings.voiceHoldToTalk,
|
|
|
|
|
_voiceAutoSendKey: settings.voiceAutoSendFinal,
|
|
|
|
|
_socketTransportModeKey: settings.socketTransportMode,
|
2025-10-30 22:44:08 +05:30
|
|
|
_quickPillsKey: settings.quickPills.toList(),
|
2025-10-01 16:55:44 +05:30
|
|
|
_sendOnEnterKey: settings.sendOnEnter,
|
2025-10-17 14:40:44 +05:30
|
|
|
PreferenceKeys.ttsSpeechRate: settings.ttsSpeechRate,
|
|
|
|
|
PreferenceKeys.ttsPitch: settings.ttsPitch,
|
|
|
|
|
PreferenceKeys.ttsVolume: settings.ttsVolume,
|
2025-10-23 16:31:15 +05:30
|
|
|
PreferenceKeys.ttsEngine: settings.ttsEngine.name,
|
2025-11-02 19:02:37 +05:30
|
|
|
PreferenceKeys.voiceSttPreference: settings.sttPreference.name,
|
2025-10-01 16:55:44 +05:30
|
|
|
};
|
|
|
|
|
|
|
|
|
|
await box.putAll(updates);
|
|
|
|
|
|
|
|
|
|
if (settings.defaultModel != null) {
|
|
|
|
|
await box.put(_defaultModelKey, settings.defaultModel);
|
|
|
|
|
} else {
|
|
|
|
|
await box.delete(_defaultModelKey);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (settings.voiceLocaleId != null && settings.voiceLocaleId!.isNotEmpty) {
|
|
|
|
|
await box.put(_voiceLocaleKey, settings.voiceLocaleId);
|
|
|
|
|
} else {
|
|
|
|
|
await box.delete(_voiceLocaleKey);
|
|
|
|
|
}
|
2025-10-17 14:40:44 +05:30
|
|
|
|
|
|
|
|
if (settings.ttsVoice != null && settings.ttsVoice!.isNotEmpty) {
|
|
|
|
|
await box.put(PreferenceKeys.ttsVoice, settings.ttsVoice);
|
|
|
|
|
} else {
|
|
|
|
|
await box.delete(PreferenceKeys.ttsVoice);
|
|
|
|
|
}
|
2025-10-23 16:31:15 +05:30
|
|
|
|
|
|
|
|
// Server-specific voice id and friendly name
|
|
|
|
|
if (settings.ttsServerVoiceId != null &&
|
|
|
|
|
settings.ttsServerVoiceId!.isNotEmpty) {
|
|
|
|
|
await box.put(PreferenceKeys.ttsServerVoiceId, settings.ttsServerVoiceId);
|
|
|
|
|
} else {
|
|
|
|
|
await box.delete(PreferenceKeys.ttsServerVoiceId);
|
|
|
|
|
}
|
|
|
|
|
if (settings.ttsServerVoiceName != null &&
|
|
|
|
|
settings.ttsServerVoiceName!.isNotEmpty) {
|
|
|
|
|
await box.put(
|
|
|
|
|
PreferenceKeys.ttsServerVoiceName,
|
|
|
|
|
settings.ttsServerVoiceName,
|
|
|
|
|
);
|
|
|
|
|
} else {
|
|
|
|
|
await box.delete(PreferenceKeys.ttsServerVoiceName);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static TtsEngine _parseTtsEngine(String? raw) {
|
|
|
|
|
switch ((raw ?? '').toLowerCase()) {
|
2025-11-02 21:31:13 +05:30
|
|
|
case 'auto':
|
|
|
|
|
case '':
|
|
|
|
|
return TtsEngine.auto;
|
2025-10-23 16:31:15 +05:30
|
|
|
case 'server':
|
|
|
|
|
return TtsEngine.server;
|
|
|
|
|
case 'device':
|
|
|
|
|
return TtsEngine.device;
|
2025-11-02 21:31:13 +05:30
|
|
|
default:
|
|
|
|
|
return TtsEngine.auto;
|
2025-10-23 16:31:15 +05:30
|
|
|
}
|
2025-08-10 01:20:45 +05:30
|
|
|
}
|
|
|
|
|
|
2025-11-02 19:02:37 +05:30
|
|
|
static SttPreference _parseSttPreference(String? raw) {
|
|
|
|
|
switch ((raw ?? '').toLowerCase()) {
|
|
|
|
|
case 'deviceonly':
|
|
|
|
|
case 'device_only':
|
|
|
|
|
case 'device':
|
|
|
|
|
return SttPreference.deviceOnly;
|
|
|
|
|
case 'serveronly':
|
|
|
|
|
case 'server_only':
|
|
|
|
|
case 'server':
|
|
|
|
|
return SttPreference.serverOnly;
|
|
|
|
|
case 'auto':
|
|
|
|
|
default:
|
|
|
|
|
return SttPreference.auto;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-22 13:54:58 +05:30
|
|
|
// Voice input specific settings
|
2025-10-01 16:55:44 +05:30
|
|
|
static Future<String?> getVoiceLocaleId() {
|
|
|
|
|
final value = _preferencesBox().get(_voiceLocaleKey) as String?;
|
|
|
|
|
return Future.value(value);
|
2025-08-22 13:54:58 +05:30
|
|
|
}
|
|
|
|
|
|
2025-10-01 16:55:44 +05:30
|
|
|
static Future<void> setVoiceLocaleId(String? localeId) {
|
|
|
|
|
final box = _preferencesBox();
|
2025-08-22 13:54:58 +05:30
|
|
|
if (localeId == null || localeId.isEmpty) {
|
2025-10-01 16:55:44 +05:30
|
|
|
return box.delete(_voiceLocaleKey);
|
2025-08-22 13:54:58 +05:30
|
|
|
}
|
2025-10-01 16:55:44 +05:30
|
|
|
return box.put(_voiceLocaleKey, localeId);
|
2025-08-22 13:54:58 +05:30
|
|
|
}
|
|
|
|
|
|
2025-10-01 16:55:44 +05:30
|
|
|
static Future<bool> getVoiceHoldToTalk() {
|
|
|
|
|
final value = _preferencesBox().get(_voiceHoldToTalkKey) as bool?;
|
|
|
|
|
return Future.value(value ?? false);
|
2025-08-22 13:54:58 +05:30
|
|
|
}
|
|
|
|
|
|
2025-10-01 16:55:44 +05:30
|
|
|
static Future<void> setVoiceHoldToTalk(bool value) {
|
|
|
|
|
return _preferencesBox().put(_voiceHoldToTalkKey, value);
|
2025-08-22 13:54:58 +05:30
|
|
|
}
|
|
|
|
|
|
2025-10-01 16:55:44 +05:30
|
|
|
static Future<bool> getVoiceAutoSendFinal() {
|
|
|
|
|
final value = _preferencesBox().get(_voiceAutoSendKey) as bool?;
|
|
|
|
|
return Future.value(value ?? false);
|
2025-08-22 13:54:58 +05:30
|
|
|
}
|
|
|
|
|
|
2025-10-01 16:55:44 +05:30
|
|
|
static Future<void> setVoiceAutoSendFinal(bool value) {
|
|
|
|
|
return _preferencesBox().put(_voiceAutoSendKey, value);
|
2025-08-22 13:54:58 +05:30
|
|
|
}
|
|
|
|
|
|
2025-10-30 22:32:59 +05:30
|
|
|
/// Transport mode: 'polling' (HTTP polling + WebSocket upgrade) or 'ws'
|
2025-10-01 16:55:44 +05:30
|
|
|
static Future<String> getSocketTransportMode() {
|
2025-10-30 22:32:59 +05:30
|
|
|
final raw = _preferencesBox().get(_socketTransportModeKey) as String?;
|
|
|
|
|
if (raw == null) {
|
|
|
|
|
return Future.value('ws');
|
|
|
|
|
}
|
|
|
|
|
if (raw == 'auto') {
|
|
|
|
|
return Future.value('polling');
|
|
|
|
|
}
|
|
|
|
|
if (raw != 'polling' && raw != 'ws') {
|
|
|
|
|
return Future.value('ws');
|
|
|
|
|
}
|
|
|
|
|
return Future.value(raw);
|
2025-09-07 11:13:05 +05:30
|
|
|
}
|
|
|
|
|
|
2025-10-01 16:55:44 +05:30
|
|
|
static Future<void> setSocketTransportMode(String mode) {
|
2025-10-30 22:32:59 +05:30
|
|
|
if (mode == 'auto') {
|
|
|
|
|
mode = 'polling';
|
|
|
|
|
}
|
|
|
|
|
if (mode != 'polling' && mode != 'ws') {
|
|
|
|
|
mode = 'polling';
|
2025-10-01 16:55:44 +05:30
|
|
|
}
|
|
|
|
|
return _preferencesBox().put(_socketTransportModeKey, mode);
|
2025-09-07 11:13:05 +05:30
|
|
|
}
|
|
|
|
|
|
2025-09-07 14:40:20 +05:30
|
|
|
// Quick Pills (visibility)
|
2025-10-01 16:55:44 +05:30
|
|
|
static Future<List<String>> getQuickPills() {
|
|
|
|
|
final stored = _preferencesBox().get(_quickPillsKey) as List<dynamic>?;
|
|
|
|
|
if (stored == null) {
|
|
|
|
|
return Future.value(const []);
|
|
|
|
|
}
|
2025-10-30 22:44:08 +05:30
|
|
|
return Future.value(List<String>.from(stored));
|
2025-09-07 14:40:20 +05:30
|
|
|
}
|
|
|
|
|
|
2025-10-01 16:55:44 +05:30
|
|
|
static Future<void> setQuickPills(List<String> pills) {
|
2025-10-30 22:44:08 +05:30
|
|
|
return _preferencesBox().put(_quickPillsKey, pills.toList());
|
2025-09-07 14:40:20 +05:30
|
|
|
}
|
|
|
|
|
|
2025-09-08 01:05:48 +05:30
|
|
|
// Chat input behavior
|
2025-10-01 16:55:44 +05:30
|
|
|
static Future<bool> getSendOnEnter() {
|
|
|
|
|
final value = _preferencesBox().get(_sendOnEnterKey) as bool?;
|
|
|
|
|
return Future.value(value ?? false);
|
2025-09-08 01:05:48 +05:30
|
|
|
}
|
|
|
|
|
|
2025-10-01 16:55:44 +05:30
|
|
|
static Future<void> setSendOnEnter(bool value) {
|
|
|
|
|
return _preferencesBox().put(_sendOnEnterKey, value);
|
2025-09-08 01:05:48 +05:30
|
|
|
}
|
|
|
|
|
|
2025-08-10 01:20:45 +05:30
|
|
|
/// Get effective animation duration considering all settings
|
|
|
|
|
static Duration getEffectiveAnimationDuration(
|
|
|
|
|
BuildContext context,
|
|
|
|
|
Duration defaultDuration,
|
|
|
|
|
AppSettings settings,
|
|
|
|
|
) {
|
|
|
|
|
// Check system reduced motion first
|
|
|
|
|
if (MediaQuery.of(context).disableAnimations || settings.reduceMotion) {
|
|
|
|
|
return Duration.zero;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Apply user animation speed preference
|
|
|
|
|
final adjustedMs =
|
|
|
|
|
(defaultDuration.inMilliseconds / settings.animationSpeed).round();
|
|
|
|
|
return Duration(milliseconds: adjustedMs.clamp(50, 1000));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Get text scale factor considering user preferences
|
|
|
|
|
static double getEffectiveTextScaleFactor(
|
|
|
|
|
BuildContext context,
|
|
|
|
|
AppSettings settings,
|
|
|
|
|
) {
|
|
|
|
|
final textScaler = MediaQuery.of(context).textScaler;
|
|
|
|
|
double baseScale = textScaler.scale(1.0);
|
|
|
|
|
|
|
|
|
|
// Apply large text preference
|
|
|
|
|
if (settings.largeText) {
|
|
|
|
|
baseScale *= 1.3;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Ensure reasonable bounds
|
|
|
|
|
return baseScale.clamp(0.8, 3.0);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-17 17:43:19 +05:30
|
|
|
/// Sentinel class to detect when defaultModel parameter is not provided
|
|
|
|
|
class _DefaultValue {
|
|
|
|
|
const _DefaultValue();
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-10 01:20:45 +05:30
|
|
|
/// Data class for app settings
|
|
|
|
|
class AppSettings {
|
|
|
|
|
final bool reduceMotion;
|
|
|
|
|
final double animationSpeed;
|
|
|
|
|
final bool hapticFeedback;
|
|
|
|
|
final bool highContrast;
|
|
|
|
|
final bool largeText;
|
|
|
|
|
final bool darkMode;
|
2025-08-17 17:01:06 +05:30
|
|
|
final String? defaultModel;
|
2025-08-22 13:54:58 +05:30
|
|
|
final String? voiceLocaleId;
|
|
|
|
|
final bool voiceHoldToTalk;
|
|
|
|
|
final bool voiceAutoSendFinal;
|
2025-10-30 22:32:59 +05:30
|
|
|
final String socketTransportMode; // 'polling' or 'ws'
|
2025-09-07 14:40:20 +05:30
|
|
|
final List<String> quickPills; // e.g., ['web','image']
|
2025-09-08 01:05:48 +05:30
|
|
|
final bool sendOnEnter;
|
2025-11-02 19:02:37 +05:30
|
|
|
final SttPreference sttPreference;
|
2025-10-17 14:40:44 +05:30
|
|
|
final String? ttsVoice;
|
|
|
|
|
final double ttsSpeechRate;
|
|
|
|
|
final double ttsPitch;
|
|
|
|
|
final double ttsVolume;
|
2025-10-23 16:31:15 +05:30
|
|
|
final TtsEngine ttsEngine;
|
|
|
|
|
final String? ttsServerVoiceId;
|
|
|
|
|
final String? ttsServerVoiceName;
|
2025-08-10 01:20:45 +05:30
|
|
|
const AppSettings({
|
|
|
|
|
this.reduceMotion = false,
|
|
|
|
|
this.animationSpeed = 1.0,
|
|
|
|
|
this.hapticFeedback = true,
|
|
|
|
|
this.highContrast = false,
|
|
|
|
|
this.largeText = false,
|
|
|
|
|
this.darkMode = true,
|
2025-08-17 17:01:06 +05:30
|
|
|
this.defaultModel,
|
2025-08-22 13:54:58 +05:30
|
|
|
this.voiceLocaleId,
|
|
|
|
|
this.voiceHoldToTalk = false,
|
|
|
|
|
this.voiceAutoSendFinal = false,
|
2025-09-07 14:51:55 +05:30
|
|
|
this.socketTransportMode = 'ws',
|
2025-09-07 14:40:20 +05:30
|
|
|
this.quickPills = const [],
|
2025-09-08 01:05:48 +05:30
|
|
|
this.sendOnEnter = false,
|
2025-11-02 19:02:37 +05:30
|
|
|
this.sttPreference = SttPreference.auto,
|
2025-10-17 14:40:44 +05:30
|
|
|
this.ttsVoice,
|
|
|
|
|
this.ttsSpeechRate = 0.5,
|
|
|
|
|
this.ttsPitch = 1.0,
|
|
|
|
|
this.ttsVolume = 1.0,
|
2025-11-02 21:31:13 +05:30
|
|
|
this.ttsEngine = TtsEngine.auto,
|
2025-10-23 16:31:15 +05:30
|
|
|
this.ttsServerVoiceId,
|
|
|
|
|
this.ttsServerVoiceName,
|
2025-08-10 01:20:45 +05:30
|
|
|
});
|
|
|
|
|
|
|
|
|
|
AppSettings copyWith({
|
|
|
|
|
bool? reduceMotion,
|
|
|
|
|
double? animationSpeed,
|
|
|
|
|
bool? hapticFeedback,
|
|
|
|
|
bool? highContrast,
|
|
|
|
|
bool? largeText,
|
|
|
|
|
bool? darkMode,
|
2025-08-17 17:43:19 +05:30
|
|
|
Object? defaultModel = const _DefaultValue(),
|
2025-08-22 13:54:58 +05:30
|
|
|
Object? voiceLocaleId = const _DefaultValue(),
|
|
|
|
|
bool? voiceHoldToTalk,
|
|
|
|
|
bool? voiceAutoSendFinal,
|
2025-09-07 11:13:05 +05:30
|
|
|
String? socketTransportMode,
|
2025-09-07 14:40:20 +05:30
|
|
|
List<String>? quickPills,
|
2025-09-08 01:05:48 +05:30
|
|
|
bool? sendOnEnter,
|
2025-11-02 19:02:37 +05:30
|
|
|
SttPreference? sttPreference,
|
2025-10-17 14:40:44 +05:30
|
|
|
Object? ttsVoice = const _DefaultValue(),
|
|
|
|
|
double? ttsSpeechRate,
|
|
|
|
|
double? ttsPitch,
|
|
|
|
|
double? ttsVolume,
|
2025-10-23 16:31:15 +05:30
|
|
|
TtsEngine? ttsEngine,
|
|
|
|
|
Object? ttsServerVoiceId = const _DefaultValue(),
|
|
|
|
|
Object? ttsServerVoiceName = const _DefaultValue(),
|
2025-08-10 01:20:45 +05:30
|
|
|
}) {
|
|
|
|
|
return AppSettings(
|
|
|
|
|
reduceMotion: reduceMotion ?? this.reduceMotion,
|
|
|
|
|
animationSpeed: animationSpeed ?? this.animationSpeed,
|
|
|
|
|
hapticFeedback: hapticFeedback ?? this.hapticFeedback,
|
|
|
|
|
highContrast: highContrast ?? this.highContrast,
|
|
|
|
|
largeText: largeText ?? this.largeText,
|
|
|
|
|
darkMode: darkMode ?? this.darkMode,
|
2025-09-21 22:31:44 +05:30
|
|
|
defaultModel: defaultModel is _DefaultValue
|
|
|
|
|
? this.defaultModel
|
|
|
|
|
: defaultModel as String?,
|
|
|
|
|
voiceLocaleId: voiceLocaleId is _DefaultValue
|
|
|
|
|
? this.voiceLocaleId
|
|
|
|
|
: voiceLocaleId as String?,
|
2025-08-22 13:54:58 +05:30
|
|
|
voiceHoldToTalk: voiceHoldToTalk ?? this.voiceHoldToTalk,
|
|
|
|
|
voiceAutoSendFinal: voiceAutoSendFinal ?? this.voiceAutoSendFinal,
|
2025-09-07 11:13:05 +05:30
|
|
|
socketTransportMode: socketTransportMode ?? this.socketTransportMode,
|
2025-09-07 14:40:20 +05:30
|
|
|
quickPills: quickPills ?? this.quickPills,
|
2025-09-08 01:05:48 +05:30
|
|
|
sendOnEnter: sendOnEnter ?? this.sendOnEnter,
|
2025-11-02 19:02:37 +05:30
|
|
|
sttPreference: sttPreference ?? this.sttPreference,
|
2025-10-17 14:40:44 +05:30
|
|
|
ttsVoice: ttsVoice is _DefaultValue ? this.ttsVoice : ttsVoice as String?,
|
|
|
|
|
ttsSpeechRate: ttsSpeechRate ?? this.ttsSpeechRate,
|
|
|
|
|
ttsPitch: ttsPitch ?? this.ttsPitch,
|
|
|
|
|
ttsVolume: ttsVolume ?? this.ttsVolume,
|
2025-10-23 16:31:15 +05:30
|
|
|
ttsEngine: ttsEngine ?? this.ttsEngine,
|
|
|
|
|
ttsServerVoiceId: ttsServerVoiceId is _DefaultValue
|
|
|
|
|
? this.ttsServerVoiceId
|
|
|
|
|
: ttsServerVoiceId as String?,
|
|
|
|
|
ttsServerVoiceName: ttsServerVoiceName is _DefaultValue
|
|
|
|
|
? this.ttsServerVoiceName
|
|
|
|
|
: ttsServerVoiceName as String?,
|
2025-08-10 01:20:45 +05:30
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
bool operator ==(Object other) {
|
|
|
|
|
if (identical(this, other)) return true;
|
|
|
|
|
return other is AppSettings &&
|
|
|
|
|
other.reduceMotion == reduceMotion &&
|
|
|
|
|
other.animationSpeed == animationSpeed &&
|
|
|
|
|
other.hapticFeedback == hapticFeedback &&
|
|
|
|
|
other.highContrast == highContrast &&
|
|
|
|
|
other.largeText == largeText &&
|
2025-08-17 17:01:06 +05:30
|
|
|
other.darkMode == darkMode &&
|
2025-08-22 13:54:58 +05:30
|
|
|
other.defaultModel == defaultModel &&
|
|
|
|
|
other.voiceLocaleId == voiceLocaleId &&
|
|
|
|
|
other.voiceHoldToTalk == voiceHoldToTalk &&
|
2025-09-07 14:40:20 +05:30
|
|
|
other.voiceAutoSendFinal == voiceAutoSendFinal &&
|
2025-11-02 19:02:37 +05:30
|
|
|
other.sttPreference == sttPreference &&
|
2025-09-08 01:05:48 +05:30
|
|
|
other.sendOnEnter == sendOnEnter &&
|
2025-10-17 14:40:44 +05:30
|
|
|
other.ttsVoice == ttsVoice &&
|
|
|
|
|
other.ttsSpeechRate == ttsSpeechRate &&
|
|
|
|
|
other.ttsPitch == ttsPitch &&
|
|
|
|
|
other.ttsVolume == ttsVolume &&
|
2025-10-23 16:31:15 +05:30
|
|
|
other.ttsEngine == ttsEngine &&
|
|
|
|
|
other.ttsServerVoiceId == ttsServerVoiceId &&
|
|
|
|
|
other.ttsServerVoiceName == ttsServerVoiceName &&
|
2025-09-07 14:40:20 +05:30
|
|
|
_listEquals(other.quickPills, quickPills);
|
2025-09-21 22:31:44 +05:30
|
|
|
// socketTransportMode intentionally not included in == to avoid frequent rebuilds
|
2025-08-10 01:20:45 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
int get hashCode {
|
2025-11-02 19:02:37 +05:30
|
|
|
return Object.hashAll([
|
2025-08-10 01:20:45 +05:30
|
|
|
reduceMotion,
|
|
|
|
|
animationSpeed,
|
|
|
|
|
hapticFeedback,
|
|
|
|
|
highContrast,
|
|
|
|
|
largeText,
|
|
|
|
|
darkMode,
|
2025-08-17 17:01:06 +05:30
|
|
|
defaultModel,
|
2025-08-22 13:54:58 +05:30
|
|
|
voiceLocaleId,
|
|
|
|
|
voiceHoldToTalk,
|
|
|
|
|
voiceAutoSendFinal,
|
2025-11-02 19:02:37 +05:30
|
|
|
sttPreference,
|
2025-09-07 11:13:05 +05:30
|
|
|
socketTransportMode,
|
2025-09-08 01:05:48 +05:30
|
|
|
sendOnEnter,
|
2025-10-17 14:40:44 +05:30
|
|
|
ttsVoice,
|
|
|
|
|
ttsSpeechRate,
|
|
|
|
|
ttsPitch,
|
|
|
|
|
ttsVolume,
|
2025-10-23 16:31:15 +05:30
|
|
|
ttsEngine,
|
|
|
|
|
ttsServerVoiceId,
|
|
|
|
|
ttsServerVoiceName,
|
2025-09-07 14:40:20 +05:30
|
|
|
Object.hashAllUnordered(quickPills),
|
2025-11-02 19:02:37 +05:30
|
|
|
]);
|
2025-08-10 01:20:45 +05:30
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-07 14:40:20 +05:30
|
|
|
bool _listEquals(List<String> a, List<String> b) {
|
|
|
|
|
if (identical(a, b)) return true;
|
|
|
|
|
if (a.length != b.length) return false;
|
|
|
|
|
for (int i = 0; i < a.length; i++) {
|
|
|
|
|
if (a[i] != b[i]) return false;
|
|
|
|
|
}
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-10 01:20:45 +05:30
|
|
|
/// Provider for app settings
|
2025-09-30 23:18:06 +05:30
|
|
|
@Riverpod(keepAlive: true)
|
2025-09-30 15:01:47 +05:30
|
|
|
class AppSettingsNotifier extends _$AppSettingsNotifier {
|
2025-09-21 22:31:44 +05:30
|
|
|
bool _initialized = false;
|
2025-08-10 01:20:45 +05:30
|
|
|
|
2025-09-21 22:31:44 +05:30
|
|
|
@override
|
|
|
|
|
AppSettings build() {
|
|
|
|
|
if (!_initialized) {
|
|
|
|
|
_initialized = true;
|
|
|
|
|
Future.microtask(_loadSettings);
|
|
|
|
|
}
|
|
|
|
|
return const AppSettings();
|
2025-08-10 01:20:45 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Future<void> _loadSettings() async {
|
|
|
|
|
final settings = await SettingsService.loadSettings();
|
2025-09-21 22:31:44 +05:30
|
|
|
if (!ref.mounted) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
2025-08-10 01:20:45 +05:30
|
|
|
state = settings;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Future<void> setReduceMotion(bool value) async {
|
|
|
|
|
state = state.copyWith(reduceMotion: value);
|
|
|
|
|
await SettingsService.setReduceMotion(value);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Future<void> setAnimationSpeed(double value) async {
|
|
|
|
|
state = state.copyWith(animationSpeed: value);
|
|
|
|
|
await SettingsService.setAnimationSpeed(value);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Future<void> setHapticFeedback(bool value) async {
|
|
|
|
|
state = state.copyWith(hapticFeedback: value);
|
|
|
|
|
await SettingsService.setHapticFeedback(value);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Future<void> setHighContrast(bool value) async {
|
|
|
|
|
state = state.copyWith(highContrast: value);
|
|
|
|
|
await SettingsService.setHighContrast(value);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Future<void> setLargeText(bool value) async {
|
|
|
|
|
state = state.copyWith(largeText: value);
|
|
|
|
|
await SettingsService.setLargeText(value);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Future<void> setDarkMode(bool value) async {
|
|
|
|
|
state = state.copyWith(darkMode: value);
|
|
|
|
|
await SettingsService.setDarkMode(value);
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-17 17:01:06 +05:30
|
|
|
Future<void> setDefaultModel(String? modelId) async {
|
|
|
|
|
state = state.copyWith(defaultModel: modelId);
|
|
|
|
|
await SettingsService.setDefaultModel(modelId);
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-22 13:54:58 +05:30
|
|
|
Future<void> setVoiceLocaleId(String? localeId) async {
|
|
|
|
|
state = state.copyWith(voiceLocaleId: localeId);
|
|
|
|
|
await SettingsService.setVoiceLocaleId(localeId);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Future<void> setVoiceHoldToTalk(bool value) async {
|
|
|
|
|
state = state.copyWith(voiceHoldToTalk: value);
|
|
|
|
|
await SettingsService.setVoiceHoldToTalk(value);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Future<void> setVoiceAutoSendFinal(bool value) async {
|
|
|
|
|
state = state.copyWith(voiceAutoSendFinal: value);
|
|
|
|
|
await SettingsService.setVoiceAutoSendFinal(value);
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-07 11:13:05 +05:30
|
|
|
Future<void> setSocketTransportMode(String mode) async {
|
2025-10-30 22:32:59 +05:30
|
|
|
var sanitized = mode;
|
|
|
|
|
if (sanitized == 'auto') {
|
|
|
|
|
sanitized = 'polling';
|
|
|
|
|
}
|
|
|
|
|
if (sanitized != 'polling' && sanitized != 'ws') {
|
|
|
|
|
sanitized = 'polling';
|
|
|
|
|
}
|
|
|
|
|
if (state.socketTransportMode != sanitized) {
|
|
|
|
|
state = state.copyWith(socketTransportMode: sanitized);
|
|
|
|
|
}
|
|
|
|
|
await SettingsService.setSocketTransportMode(sanitized);
|
2025-09-07 11:13:05 +05:30
|
|
|
}
|
|
|
|
|
|
2025-09-07 14:40:20 +05:30
|
|
|
Future<void> setQuickPills(List<String> pills) async {
|
2025-10-30 22:44:08 +05:30
|
|
|
// Accept arbitrary server tool IDs plus built-ins
|
|
|
|
|
// Platform-specific limits are enforced in the UI layer
|
|
|
|
|
state = state.copyWith(quickPills: pills);
|
|
|
|
|
await SettingsService.setQuickPills(pills);
|
2025-09-07 14:40:20 +05:30
|
|
|
}
|
|
|
|
|
|
2025-09-08 01:05:48 +05:30
|
|
|
Future<void> setSendOnEnter(bool value) async {
|
|
|
|
|
state = state.copyWith(sendOnEnter: value);
|
|
|
|
|
await SettingsService.setSendOnEnter(value);
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-02 19:02:37 +05:30
|
|
|
Future<void> setSttPreference(SttPreference preference) async {
|
|
|
|
|
if (state.sttPreference == preference) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
state = state.copyWith(sttPreference: preference);
|
|
|
|
|
await SettingsService.saveSettings(state);
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-17 14:40:44 +05:30
|
|
|
Future<void> setTtsVoice(String? voice) async {
|
|
|
|
|
state = state.copyWith(ttsVoice: voice);
|
|
|
|
|
await SettingsService.saveSettings(state);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Future<void> setTtsSpeechRate(double rate) async {
|
|
|
|
|
state = state.copyWith(ttsSpeechRate: rate);
|
|
|
|
|
await SettingsService.saveSettings(state);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Future<void> setTtsPitch(double pitch) async {
|
|
|
|
|
state = state.copyWith(ttsPitch: pitch);
|
|
|
|
|
await SettingsService.saveSettings(state);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Future<void> setTtsVolume(double volume) async {
|
|
|
|
|
state = state.copyWith(ttsVolume: volume);
|
|
|
|
|
await SettingsService.saveSettings(state);
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-23 16:31:15 +05:30
|
|
|
Future<void> setTtsEngine(TtsEngine engine) async {
|
|
|
|
|
state = state.copyWith(ttsEngine: engine);
|
|
|
|
|
await SettingsService.saveSettings(state);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Future<void> setTtsServerVoiceName(String? name) async {
|
|
|
|
|
state = state.copyWith(ttsServerVoiceName: name);
|
|
|
|
|
await SettingsService.saveSettings(state);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Future<void> setTtsServerVoiceId(String? id) async {
|
|
|
|
|
state = state.copyWith(ttsServerVoiceId: id);
|
|
|
|
|
await SettingsService.saveSettings(state);
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-10 01:20:45 +05:30
|
|
|
Future<void> resetToDefaults() async {
|
|
|
|
|
const defaultSettings = AppSettings();
|
|
|
|
|
await SettingsService.saveSettings(defaultSettings);
|
|
|
|
|
state = defaultSettings;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Provider for checking if haptic feedback should be enabled
|
|
|
|
|
final hapticEnabledProvider = Provider<bool>((ref) {
|
|
|
|
|
final settings = ref.watch(appSettingsProvider);
|
|
|
|
|
return settings.hapticFeedback;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
/// Provider for effective animation settings
|
|
|
|
|
final effectiveAnimationSettingsProvider = Provider<AnimationSettings>((ref) {
|
|
|
|
|
final appSettings = ref.watch(appSettingsProvider);
|
|
|
|
|
|
|
|
|
|
return AnimationSettings(
|
|
|
|
|
reduceMotion: appSettings.reduceMotion,
|
|
|
|
|
performance: AnimationPerformance.adaptive,
|
|
|
|
|
animationSpeed: appSettings.animationSpeed,
|
|
|
|
|
);
|
|
|
|
|
});
|