| 1 | import 'dart:convert'; |
| 2 | |
| 3 | import 'package:flutter/material.dart'; |
| 4 | import 'package:flutter_gemma/flutter_gemma.dart'; |
| 5 | |
| 6 | import '../../../../core/models/enums.dart'; |
| 7 | import '../../../../features/chat/data/models/mcp_integration.dart'; |
| 8 | import '../../../../features/chat/utils/image_upload_utils.dart'; |
| 9 | import '../../../../features/tts/data/kitten_tts_model.dart'; |
| 10 | |
| 11 | enum SyntaxThemeName { light, dark } |
| 12 | |
| 13 | class AppSettings { |
| 14 | static const Object _unset = Object(); |
| 15 | |
| 16 | final double temperature; |
| 17 | final double topP; |
| 18 | final int maxTokens; |
| 19 | final int contextLength; |
| 20 | final ThemeMode themeMode; |
| 21 | final double fontSize; |
| 22 | final bool showSystemMessages; |
| 23 | final bool hapticFeedbackEnabled; |
| 24 | final bool sendOnEnter; |
| 25 | final String? defaultServerId; |
| 26 | final bool showDataIndicator; |
| 27 | final bool autoGenerateTitle; |
| 28 | final bool streamingEnabled; |
| 29 | final String? defaultPersonaId; |
| 30 | final String? defaultModelId; |
| 31 | final String? defaultModelServerId; |
| 32 | final bool hasCompletedOnboarding; |
| 33 | final bool hasAskedForNotifications; |
| 34 | final bool mcpEnabled; |
| 35 | final bool newChatMcpEnabled; |
| 36 | final SyntaxThemeName codeThemeDark; |
| 37 | final SyntaxThemeName codeThemeLight; |
| 38 | final PreferredBackend preferredBackend; |
| 39 | final EngineId ttsEngine; |
| 40 | final String? ttsVoiceId; |
| 41 | final double ttsSpeed; |
| 42 | final KittenTtsModelVariant kittenTtsModelVariant; |
| 43 | final bool autoSpeakEnabled; |
| 44 | final bool conciseVoiceResponsesEnabled; |
| 45 | final bool ttsProcessMarkdown; |
| 46 | final int ttsSkipSeconds; |
| 47 | final bool smartReplyEnabled; |
| 48 | final bool aiUserResponseEnabled; |
| 49 | final String? localeCode; |
| 50 | final String? huggingFaceToken; |
| 51 | final bool unloadModelsBeforeLoad; |
| 52 | final bool tempChatKeyboardIncognito; |
| 53 | final bool resumeLastChat; |
| 54 | final bool imageCompressionEnabled; |
| 55 | final ImageCompressionLevel imageCompressionLevel; |
| 56 | final bool smartRepliesUsePersona; |
| 57 | final bool keepPersonaOnNewChat; |
| 58 | final bool roleSwapButtonEnabled; |
| 59 | final bool showSystemMessagesInChat; |
| 60 | final bool calendarToolsEnabled; |
| 61 | final bool locationToolsEnabled; |
| 62 | final bool autoCollapseThinking; |
| 63 | |
| 64 | /// Whether `temperature` / `top_p` are sent to remote APIs. Some providers |
| 65 | /// (e.g. reasoning models) reject them (#81). |
| 66 | final bool sendTemperature; |
| 67 | final bool sendTopP; |
| 68 | |
| 69 | /// Fallback system prompt used when a chat has neither its own prompt nor |
| 70 | /// a persona. |
| 71 | final String defaultSystemPrompt; |
| 72 | final List<McpIntegration> savedMcpIntegrations; |
| 73 | |
| 74 | AppSettings({ |
| 75 | this.temperature = 0.7, |
| 76 | this.topP = 0.9, |
| 77 | this.maxTokens = 2048, |
| 78 | this.contextLength = 4096, |
| 79 | this.themeMode = ThemeMode.dark, |
| 80 | this.fontSize = 16.0, |
| 81 | this.showSystemMessages = false, |
| 82 | this.hapticFeedbackEnabled = true, |
| 83 | this.sendOnEnter = false, |
| 84 | this.defaultServerId, |
| 85 | this.showDataIndicator = true, |
| 86 | this.autoGenerateTitle = true, |
| 87 | this.streamingEnabled = true, |
| 88 | this.defaultPersonaId, |
| 89 | this.defaultModelId, |
| 90 | this.defaultModelServerId, |
| 91 | this.hasCompletedOnboarding = false, |
| 92 | this.hasAskedForNotifications = false, |
| 93 | this.mcpEnabled = false, |
| 94 | this.newChatMcpEnabled = false, |
| 95 | this.codeThemeDark = SyntaxThemeName.dark, |
| 96 | this.codeThemeLight = SyntaxThemeName.light, |
| 97 | this.preferredBackend = PreferredBackend.cpu, |
| 98 | this.ttsEngine = EngineId.system, |
| 99 | this.ttsVoiceId, |
| 100 | this.ttsSpeed = 1.0, |
| 101 | this.kittenTtsModelVariant = KittenTtsModelVariant.nanoInt8, |
| 102 | this.autoSpeakEnabled = false, |
| 103 | this.conciseVoiceResponsesEnabled = true, |
| 104 | this.ttsProcessMarkdown = true, |
| 105 | this.ttsSkipSeconds = 10, |
| 106 | this.smartReplyEnabled = true, |
| 107 | this.aiUserResponseEnabled = false, |
| 108 | this.localeCode, |
| 109 | this.huggingFaceToken, |
| 110 | this.unloadModelsBeforeLoad = false, |
| 111 | this.tempChatKeyboardIncognito = true, |
| 112 | this.resumeLastChat = true, |
| 113 | this.imageCompressionEnabled = false, |
| 114 | this.imageCompressionLevel = ImageCompressionLevel.medium, |
| 115 | this.smartRepliesUsePersona = false, |
| 116 | this.keepPersonaOnNewChat = false, |
| 117 | this.roleSwapButtonEnabled = false, |
| 118 | this.showSystemMessagesInChat = true, |
| 119 | this.calendarToolsEnabled = false, |
| 120 | this.locationToolsEnabled = false, |
| 121 | this.autoCollapseThinking = false, |
| 122 | this.sendTemperature = true, |
| 123 | this.sendTopP = true, |
| 124 | this.defaultSystemPrompt = '', |
| 125 | this.savedMcpIntegrations = const [], |
| 126 | }); |
| 127 | |
| 128 | AppSettings copyWith({ |
| 129 | double? temperature, |
| 130 | double? topP, |
| 131 | int? maxTokens, |
| 132 | int? contextLength, |
| 133 | ThemeMode? themeMode, |
| 134 | double? fontSize, |
| 135 | bool? showSystemMessages, |
| 136 | bool? hapticFeedbackEnabled, |
| 137 | bool? sendOnEnter, |
| 138 | String? defaultServerId, |
| 139 | bool? showDataIndicator, |
| 140 | bool? autoGenerateTitle, |
| 141 | bool? streamingEnabled, |
| 142 | String? defaultPersonaId, |
| 143 | Object? defaultModelId = _unset, |
| 144 | Object? defaultModelServerId = _unset, |
| 145 | bool? hasCompletedOnboarding, |
| 146 | bool? hasAskedForNotifications, |
| 147 | bool? mcpEnabled, |
| 148 | bool? newChatMcpEnabled, |
| 149 | SyntaxThemeName? codeThemeDark, |
| 150 | SyntaxThemeName? codeThemeLight, |
| 151 | PreferredBackend? preferredBackend, |
| 152 | EngineId? ttsEngine, |
| 153 | Object? ttsVoiceId = _unset, |
| 154 | double? ttsSpeed, |
| 155 | KittenTtsModelVariant? kittenTtsModelVariant, |
| 156 | bool? autoSpeakEnabled, |
| 157 | bool? conciseVoiceResponsesEnabled, |
| 158 | bool? ttsProcessMarkdown, |
| 159 | int? ttsSkipSeconds, |
| 160 | bool? smartReplyEnabled, |
| 161 | bool? aiUserResponseEnabled, |
| 162 | Object? localeCode = _unset, |
| 163 | Object? huggingFaceToken = _unset, |
| 164 | bool? unloadModelsBeforeLoad, |
| 165 | bool? tempChatKeyboardIncognito, |
| 166 | bool? resumeLastChat, |
| 167 | bool? imageCompressionEnabled, |
| 168 | ImageCompressionLevel? imageCompressionLevel, |
| 169 | bool? smartRepliesUsePersona, |
| 170 | bool? keepPersonaOnNewChat, |
| 171 | bool? roleSwapButtonEnabled, |
| 172 | bool? showSystemMessagesInChat, |
| 173 | bool? calendarToolsEnabled, |
| 174 | bool? locationToolsEnabled, |
| 175 | bool? autoCollapseThinking, |
| 176 | bool? sendTemperature, |
| 177 | bool? sendTopP, |
| 178 | String? defaultSystemPrompt, |
| 179 | List<McpIntegration>? savedMcpIntegrations, |
| 180 | }) { |
| 181 | return AppSettings( |
| 182 | temperature: temperature ?? this.temperature, |
| 183 | topP: topP ?? this.topP, |
| 184 | maxTokens: maxTokens ?? this.maxTokens, |
| 185 | contextLength: contextLength ?? this.contextLength, |
| 186 | themeMode: themeMode ?? this.themeMode, |
| 187 | fontSize: fontSize ?? this.fontSize, |
| 188 | showSystemMessages: showSystemMessages ?? this.showSystemMessages, |
| 189 | hapticFeedbackEnabled: |
| 190 | hapticFeedbackEnabled ?? this.hapticFeedbackEnabled, |
| 191 | sendOnEnter: sendOnEnter ?? this.sendOnEnter, |
| 192 | defaultServerId: defaultServerId ?? this.defaultServerId, |
| 193 | showDataIndicator: showDataIndicator ?? this.showDataIndicator, |
| 194 | autoGenerateTitle: autoGenerateTitle ?? this.autoGenerateTitle, |
| 195 | streamingEnabled: streamingEnabled ?? this.streamingEnabled, |
| 196 | defaultPersonaId: defaultPersonaId ?? this.defaultPersonaId, |
| 197 | defaultModelId: identical(defaultModelId, _unset) |
| 198 | ? this.defaultModelId |
| 199 | : defaultModelId as String?, |
| 200 | defaultModelServerId: identical(defaultModelServerId, _unset) |
| 201 | ? this.defaultModelServerId |
| 202 | : defaultModelServerId as String?, |
| 203 | hasCompletedOnboarding: |
| 204 | hasCompletedOnboarding ?? this.hasCompletedOnboarding, |
| 205 | hasAskedForNotifications: |
| 206 | hasAskedForNotifications ?? this.hasAskedForNotifications, |
| 207 | mcpEnabled: mcpEnabled ?? this.mcpEnabled, |
| 208 | newChatMcpEnabled: newChatMcpEnabled ?? this.newChatMcpEnabled, |
| 209 | codeThemeDark: codeThemeDark ?? this.codeThemeDark, |
| 210 | codeThemeLight: codeThemeLight ?? this.codeThemeLight, |
| 211 | preferredBackend: preferredBackend ?? this.preferredBackend, |
| 212 | ttsEngine: ttsEngine ?? this.ttsEngine, |
| 213 | ttsVoiceId: identical(ttsVoiceId, _unset) |
| 214 | ? this.ttsVoiceId |
| 215 | : ttsVoiceId as String?, |
| 216 | ttsSpeed: ttsSpeed ?? this.ttsSpeed, |
| 217 | kittenTtsModelVariant: |
| 218 | kittenTtsModelVariant ?? this.kittenTtsModelVariant, |
| 219 | autoSpeakEnabled: autoSpeakEnabled ?? this.autoSpeakEnabled, |
| 220 | conciseVoiceResponsesEnabled: |
| 221 | conciseVoiceResponsesEnabled ?? this.conciseVoiceResponsesEnabled, |
| 222 | ttsProcessMarkdown: ttsProcessMarkdown ?? this.ttsProcessMarkdown, |
| 223 | ttsSkipSeconds: ttsSkipSeconds ?? this.ttsSkipSeconds, |
| 224 | smartReplyEnabled: smartReplyEnabled ?? this.smartReplyEnabled, |
| 225 | aiUserResponseEnabled: |
| 226 | aiUserResponseEnabled ?? this.aiUserResponseEnabled, |
| 227 | localeCode: identical(localeCode, _unset) |
| 228 | ? this.localeCode |
| 229 | : localeCode as String?, |
| 230 | huggingFaceToken: identical(huggingFaceToken, _unset) |
| 231 | ? this.huggingFaceToken |
| 232 | : huggingFaceToken as String?, |
| 233 | unloadModelsBeforeLoad: |
| 234 | unloadModelsBeforeLoad ?? this.unloadModelsBeforeLoad, |
| 235 | tempChatKeyboardIncognito: |
| 236 | tempChatKeyboardIncognito ?? this.tempChatKeyboardIncognito, |
| 237 | resumeLastChat: resumeLastChat ?? this.resumeLastChat, |
| 238 | imageCompressionEnabled: |
| 239 | imageCompressionEnabled ?? this.imageCompressionEnabled, |
| 240 | imageCompressionLevel: |
| 241 | imageCompressionLevel ?? this.imageCompressionLevel, |
| 242 | smartRepliesUsePersona: |
| 243 | smartRepliesUsePersona ?? this.smartRepliesUsePersona, |
| 244 | keepPersonaOnNewChat: keepPersonaOnNewChat ?? this.keepPersonaOnNewChat, |
| 245 | roleSwapButtonEnabled: |
| 246 | roleSwapButtonEnabled ?? this.roleSwapButtonEnabled, |
| 247 | showSystemMessagesInChat: |
| 248 | showSystemMessagesInChat ?? this.showSystemMessagesInChat, |
| 249 | calendarToolsEnabled: calendarToolsEnabled ?? this.calendarToolsEnabled, |
| 250 | locationToolsEnabled: locationToolsEnabled ?? this.locationToolsEnabled, |
| 251 | autoCollapseThinking: autoCollapseThinking ?? this.autoCollapseThinking, |
| 252 | sendTemperature: sendTemperature ?? this.sendTemperature, |
| 253 | sendTopP: sendTopP ?? this.sendTopP, |
| 254 | defaultSystemPrompt: defaultSystemPrompt ?? this.defaultSystemPrompt, |
| 255 | savedMcpIntegrations: savedMcpIntegrations ?? this.savedMcpIntegrations, |
| 256 | ); |
| 257 | } |
| 258 | |
| 259 | Map<String, dynamic> toMap() { |
| 260 | return { |
| 261 | 'temperature': temperature, |
| 262 | 'topP': topP, |
| 263 | 'maxTokens': maxTokens, |
| 264 | 'contextLength': contextLength, |
| 265 | 'themeMode': themeMode.index, |
| 266 | 'fontSize': fontSize, |
| 267 | 'showSystemMessages': showSystemMessages, |
| 268 | 'hapticFeedbackEnabled': hapticFeedbackEnabled, |
| 269 | 'sendOnEnter': sendOnEnter, |
| 270 | 'defaultServerId': defaultServerId, |
| 271 | 'showDataIndicator': showDataIndicator, |
| 272 | 'autoGenerateTitle': autoGenerateTitle, |
| 273 | 'streamingEnabled': streamingEnabled, |
| 274 | 'defaultPersonaId': defaultPersonaId, |
| 275 | 'defaultModelId': defaultModelId, |
| 276 | 'defaultModelServerId': defaultModelServerId, |
| 277 | 'hasCompletedOnboarding': hasCompletedOnboarding, |
| 278 | 'hasAskedForNotifications': hasAskedForNotifications, |
| 279 | 'mcpEnabled': mcpEnabled, |
| 280 | 'newChatMcpEnabled': newChatMcpEnabled, |
| 281 | 'codeThemeDark': codeThemeDark.index, |
| 282 | 'codeThemeLight': codeThemeLight.index, |
| 283 | 'preferredBackend': preferredBackend.index, |
| 284 | 'ttsEngine': ttsEngine.name, |
| 285 | 'ttsVoiceId': ttsVoiceId, |
| 286 | 'ttsSpeed': ttsSpeed, |
| 287 | 'kittenTtsModelVariant': kittenTtsModelVariant.name, |
| 288 | 'autoSpeakEnabled': autoSpeakEnabled, |
| 289 | 'conciseVoiceResponsesEnabled': conciseVoiceResponsesEnabled, |
| 290 | 'ttsProcessMarkdown': ttsProcessMarkdown, |
| 291 | 'ttsSkipSeconds': ttsSkipSeconds, |
| 292 | 'smartReplyEnabled': smartReplyEnabled, |
| 293 | 'aiUserResponseEnabled': aiUserResponseEnabled, |
| 294 | 'localeCode': localeCode, |
| 295 | 'huggingFaceToken': huggingFaceToken, |
| 296 | 'unloadModelsBeforeLoad': unloadModelsBeforeLoad, |
| 297 | 'tempChatKeyboardIncognito': tempChatKeyboardIncognito, |
| 298 | 'resumeLastChat': resumeLastChat, |
| 299 | 'imageCompressionEnabled': imageCompressionEnabled, |
| 300 | 'imageCompressionLevel': imageCompressionLevel.name, |
| 301 | 'smartRepliesUsePersona': smartRepliesUsePersona, |
| 302 | 'keepPersonaOnNewChat': keepPersonaOnNewChat, |
| 303 | 'roleSwapButtonEnabled': roleSwapButtonEnabled, |
| 304 | 'showSystemMessagesInChat': showSystemMessagesInChat, |
| 305 | 'calendarToolsEnabled': calendarToolsEnabled, |
| 306 | 'locationToolsEnabled': locationToolsEnabled, |
| 307 | 'autoCollapseThinking': autoCollapseThinking, |
| 308 | 'sendTemperature': sendTemperature, |
| 309 | 'sendTopP': sendTopP, |
| 310 | 'defaultSystemPrompt': defaultSystemPrompt, |
| 311 | 'savedMcpIntegrations': savedMcpIntegrations |
| 312 | .map((i) => i.toJson()) |
| 313 | .toList(), |
| 314 | }; |
| 315 | } |
| 316 | |
| 317 | factory AppSettings.fromMap(Map<String, dynamic> map) { |
| 318 | return AppSettings( |
| 319 | temperature: map['temperature']?.toDouble() ?? 0.7, |
| 320 | topP: map['topP']?.toDouble() ?? 0.9, |
| 321 | maxTokens: map['maxTokens']?.toInt() ?? 2048, |
| 322 | contextLength: map['contextLength']?.toInt() ?? 4096, |
| 323 | themeMode: ThemeMode.values[map['themeMode'] ?? 2], |
| 324 | fontSize: map['fontSize']?.toDouble() ?? 16.0, |
| 325 | showSystemMessages: map['showSystemMessages'] ?? false, |
| 326 | hapticFeedbackEnabled: map['hapticFeedbackEnabled'] ?? true, |
| 327 | sendOnEnter: map['sendOnEnter'] ?? false, |
| 328 | defaultServerId: map['defaultServerId'], |
| 329 | showDataIndicator: map['showDataIndicator'] ?? true, |
| 330 | autoGenerateTitle: map['autoGenerateTitle'] ?? true, |
| 331 | streamingEnabled: map['streamingEnabled'] ?? true, |
| 332 | defaultPersonaId: map['defaultPersonaId'], |
| 333 | defaultModelId: map['defaultModelId'] as String?, |
| 334 | defaultModelServerId: map['defaultModelServerId'] as String?, |
| 335 | hasCompletedOnboarding: map['hasCompletedOnboarding'] ?? false, |
| 336 | hasAskedForNotifications: map['hasAskedForNotifications'] ?? false, |
| 337 | mcpEnabled: map['mcpEnabled'] ?? false, |
| 338 | newChatMcpEnabled: map['newChatMcpEnabled'] ?? false, |
| 339 | codeThemeDark: SyntaxThemeName.values[map['codeThemeDark'] ?? 0], |
| 340 | codeThemeLight: SyntaxThemeName.values[map['codeThemeLight'] ?? 1], |
| 341 | preferredBackend: _parsePreferredBackend(map['preferredBackend']), |
| 342 | ttsEngine: _parseEngine(map['ttsEngine']), |
| 343 | ttsVoiceId: map['ttsVoiceId'], |
| 344 | ttsSpeed: map['ttsSpeed']?.toDouble() ?? 1.0, |
| 345 | kittenTtsModelVariant: _parseKittenVariant(map['kittenTtsModelVariant']), |
| 346 | autoSpeakEnabled: map['autoSpeakEnabled'] ?? false, |
| 347 | conciseVoiceResponsesEnabled: map['conciseVoiceResponsesEnabled'] ?? true, |
| 348 | ttsProcessMarkdown: map['ttsProcessMarkdown'] ?? true, |
| 349 | ttsSkipSeconds: _parseTtsSkipSeconds(map['ttsSkipSeconds']), |
| 350 | smartReplyEnabled: map['smartReplyEnabled'] ?? true, |
| 351 | aiUserResponseEnabled: map['aiUserResponseEnabled'] ?? false, |
| 352 | localeCode: map['localeCode'] as String?, |
| 353 | huggingFaceToken: map['huggingFaceToken'] as String?, |
| 354 | unloadModelsBeforeLoad: map['unloadModelsBeforeLoad'] ?? false, |
| 355 | tempChatKeyboardIncognito: map['tempChatKeyboardIncognito'] ?? true, |
| 356 | resumeLastChat: map['resumeLastChat'] ?? true, |
| 357 | imageCompressionEnabled: map['imageCompressionEnabled'] ?? false, |
| 358 | imageCompressionLevel: _parseImageCompressionLevel( |
| 359 | map['imageCompressionLevel'], |
| 360 | ), |
| 361 | smartRepliesUsePersona: map['smartRepliesUsePersona'] ?? false, |
| 362 | keepPersonaOnNewChat: map['keepPersonaOnNewChat'] ?? false, |
| 363 | roleSwapButtonEnabled: map['roleSwapButtonEnabled'] ?? false, |
| 364 | showSystemMessagesInChat: map['showSystemMessagesInChat'] ?? true, |
| 365 | calendarToolsEnabled: map['calendarToolsEnabled'] ?? false, |
| 366 | locationToolsEnabled: map['locationToolsEnabled'] ?? false, |
| 367 | autoCollapseThinking: map['autoCollapseThinking'] ?? false, |
| 368 | sendTemperature: map['sendTemperature'] ?? true, |
| 369 | sendTopP: map['sendTopP'] ?? true, |
| 370 | defaultSystemPrompt: map['defaultSystemPrompt'] as String? ?? '', |
| 371 | savedMcpIntegrations: _parseSavedMcpIntegrations( |
| 372 | map['savedMcpIntegrations'], |
| 373 | ), |
| 374 | ); |
| 375 | } |
| 376 | |
| 377 | static List<McpIntegration> _parseSavedMcpIntegrations(dynamic value) { |
| 378 | if (value is List) { |
| 379 | return value |
| 380 | .whereType<Map<String, dynamic>>() |
| 381 | .map((m) => McpIntegration.fromJson(m)) |
| 382 | .toList(); |
| 383 | } |
| 384 | return const []; |
| 385 | } |
| 386 | |
| 387 | static ImageCompressionLevel _parseImageCompressionLevel(dynamic value) { |
| 388 | if (value is String) { |
| 389 | try { |
| 390 | return ImageCompressionLevel.values.byName(value); |
| 391 | } catch (_) {} |
| 392 | } |
| 393 | return ImageCompressionLevel.medium; |
| 394 | } |
| 395 | |
| 396 | static PreferredBackend _parsePreferredBackend(dynamic value) { |
| 397 | if (value is int && value < PreferredBackend.values.length) { |
| 398 | return PreferredBackend.values[value]; |
| 399 | } |
| 400 | // Handle migration from old LiteLmBackendType (had cpu=0, gpu=1, npu=2) |
| 401 | // Map npu (2) to gpu since flutter_gemma only has cpu/gpu |
| 402 | if (value is int && value == 2) return PreferredBackend.gpu; |
| 403 | return PreferredBackend.cpu; |
| 404 | } |
| 405 | |
| 406 | static EngineId _parseEngine(dynamic value) { |
| 407 | if (value is String) { |
| 408 | return engineIdFromString(value); |
| 409 | } |
| 410 | if (value is int && value >= 0 && value < EngineId.values.length) { |
| 411 | return EngineId.values[value]; |
| 412 | } |
| 413 | return EngineId.system; |
| 414 | } |
| 415 | |
| 416 | static KittenTtsModelVariant _parseKittenVariant(dynamic value) { |
| 417 | if (value is String) { |
| 418 | try { |
| 419 | return KittenTtsModelVariant.values.byName(value); |
| 420 | } catch (_) {} |
| 421 | } |
| 422 | return KittenTtsModelVariant.nanoInt8; |
| 423 | } |
| 424 | |
| 425 | static int _parseTtsSkipSeconds(dynamic value) { |
| 426 | const allowed = {5, 10, 15, 30}; |
| 427 | if (value is int && allowed.contains(value)) return value; |
| 428 | return 10; |
| 429 | } |
| 430 | |
| 431 | String toJson() => json.encode(toMap()); |
| 432 | |
| 433 | factory AppSettings.fromJson(String source) => |
| 434 | AppSettings.fromMap(json.decode(source)); |
| 435 | } |