import 'dart:convert'; import 'dart:async'; import 'package:shitman/attributes/serializable.dart'; /// Enum for supported setting value types. /// /// This enum is used internally to track the type of each setting /// and ensure proper type casting during storage and retrieval operations. enum SettingType { /// Boolean true/false values bool, /// Integer numeric values int, /// Double-precision floating point values double, /// String text values string, } /// Abstract base class for all setting types. /// /// This class defines the common interface and functionality for all settings, /// including type safety, validation, change notifications, and metadata. /// /// Type parameter [T] ensures compile-time type safety for setting values. /// /// Example usage: /// ```dart /// // Create a validated volume setting /// final volumeSetting = DoubleSetting( /// key: 'volume', /// defaultValue: 0.5, /// validator: (value) => value >= 0.0 && value <= 1.0, /// ); /// /// // Listen for changes /// volumeSetting.stream.listen((newValue) { /// print('Volume changed to: $newValue'); /// }); /// ``` /// /// Concrete implementations: /// - [BoolSetting] for boolean values /// - [IntSetting] for integer values /// - [DoubleSetting] for floating-point values /// - [StringSetting] for text values abstract class Setting implements Serializable { /// Internal stream controller for broadcasting value changes. /// Uses broadcast to allow multiple listeners. final StreamController _controller = StreamController.broadcast(); /// Unique identifier for this setting within its group. /// /// Keys should be descriptive and follow camelCase convention. /// Examples: 'soundEnabled', 'maxRetries', 'serverUrl' final String key; /// The data type of this setting's value. /// /// Used internally for type checking and storage operations. /// Automatically set by concrete implementations. final SettingType type; /// The default value used when the setting hasn't been explicitly set. /// /// This value is used during initialization and reset operations. /// Must match the generic type parameter [T]. final T defaultValue; /// Whether this setting can be modified by user code. /// /// When false, attempts to modify the setting will throw /// [SettingNotConfigurableException]. Useful for system settings /// or read-only configuration values. /// /// Defaults to true. final bool userConfigurable; /// Optional function to validate setting values before storage. /// /// The validator receives the new value and should return: /// - `true` if the value is valid /// - `false` if the value should be rejected /// /// When validation fails, [SettingValidationException] is thrown. /// /// Example: /// ```dart /// validator: (value) => value >= 0 && value <= 100 /// ``` final bool Function(T)? validator; /// Stream that emits new values when the setting changes. /// /// This stream uses broadcast semantics, allowing multiple listeners. /// The stream emits the new value immediately after it's stored. /// /// Example: /// ```dart /// setting.stream.listen((newValue) { /// print('Setting changed to: $newValue'); /// updateUI(newValue); /// }); /// ``` Stream get stream => _controller.stream; /// Creates a new setting with the specified configuration. /// /// Parameters: /// - [key]: Unique identifier within the settings group /// - [type]: Data type of the setting value /// - [defaultValue]: Initial/reset value for the setting /// - [userConfigurable]: Whether the setting can be modified (default: true) /// - [validator]: Optional validation function for new values Setting({ required this.key, required this.type, required this.defaultValue, this.userConfigurable = true, this.validator, }); /// Internal method to notify all stream listeners of a value change. /// /// This method is called automatically by the settings framework /// after a value has been successfully stored. Application code /// should not call this method directly. /// /// Parameters: /// - [value]: The new value that was stored void notifyChange(T value) { _controller.add(value); } /// Internal method to validate a value using the validator function. /// /// Returns true if no validator is provided or if the validator /// function returns true. Returns false if validation fails. /// /// Parameters: /// - [value]: The value to validate /// /// Returns: true if valid, false if invalid bool validate(T value) { return validator?.call(value) ?? true; } /// Dispose of the stream controller and release resources. /// /// This method should be called when the setting is no longer needed /// to prevent memory leaks. It's automatically called by the settings /// framework when disposing of setting groups. /// /// After calling dispose, the [stream] will no longer emit events. void dispose() { _controller.close(); } /// Converts the setting to a map. @override Map toMap() { return { 'key': key, 'type': type.name, 'defaultValue': defaultValue, 'userConfigurable': userConfigurable, // Todo: convert validator to use validation classes (e.g. RangeValidator) 'validator': null, }; } /// Creates a setting from a map representation. Setting.fromMap(Map map) : key = map['key'] as String, type = SettingType.values.firstWhere( (e) => e.name == map['type'], orElse: () => throw ArgumentError('Invalid setting type: ${map['type']}'), ), defaultValue = map['defaultValue'] as T, userConfigurable = map['userConfigurable'] as bool? ?? true, validator = null; /// Converts the setting to a JSON string representation. @override String toJson() { return jsonEncode(toMap()); } /// Creates a setting from a JSON string representation. Setting.fromJson(String json) : this.fromMap(jsonDecode(json) as Map); } /// A setting that stores boolean (true/false) values. /// /// This is a concrete implementation of [Setting] specialized for boolean values. /// Commonly used for feature flags, toggles, and binary preferences. /// /// Example: /// ```dart /// final soundEnabled = BoolSetting( /// key: 'soundEnabled', /// defaultValue: true, /// ); /// /// final debugMode = BoolSetting( /// key: 'debugMode', /// defaultValue: false, /// userConfigurable: false, // System setting /// ); /// ``` class BoolSetting extends Setting { /// Creates a new boolean setting. /// /// Parameters: /// - [key]: Unique identifier for this setting /// - [defaultValue]: Initial boolean value (true or false) /// - [userConfigurable]: Whether users can modify this setting (default: true) /// - [validator]: Optional validation function for boolean values BoolSetting({ required super.key, required super.defaultValue, super.userConfigurable, super.validator, }) : super(type: SettingType.bool); /// Converts the boolean value to a JSON string representation. @override String toJson() { return defaultValue.toString(); } /// Creates a boolean setting from a JSON string representation. } /// A setting that stores integer numeric values. /// /// This is a concrete implementation of [Setting] specialized for integer values. /// Useful for counts, limits, indices, and whole number preferences. /// /// Example: /// ```dart /// final maxRetries = IntSetting( /// key: 'maxRetries', /// defaultValue: 3, /// validator: (value) => value >= 0 && value <= 10, /// ); /// /// final fontSize = IntSetting( /// key: 'fontSize', /// defaultValue: 14, /// validator: (value) => value >= 8 && value <= 72, /// ); /// ``` class IntSetting extends Setting { /// Creates a new integer setting. /// /// Parameters: /// - [key]: Unique identifier for this setting /// - [defaultValue]: Initial integer value /// - [userConfigurable]: Whether users can modify this setting (default: true) /// - [validator]: Optional validation function (e.g., range checking) IntSetting({ required super.key, required super.defaultValue, super.userConfigurable, super.validator, }) : super(type: SettingType.int); } /// A setting that stores double-precision floating-point values. /// /// This is a concrete implementation of [Setting] specialized for decimal values. /// Perfect for percentages, ratios, measurements, and precise numeric settings. /// /// Example: /// ```dart /// final volume = DoubleSetting( /// key: 'volume', /// defaultValue: 0.8, /// validator: (value) => value >= 0.0 && value <= 1.0, /// ); /// /// final animationSpeed = DoubleSetting( /// key: 'animationSpeed', /// defaultValue: 1.0, /// validator: (value) => value > 0.0 && value <= 5.0, /// ); /// ``` class DoubleSetting extends Setting { /// Creates a new double setting. /// /// Parameters: /// - [key]: Unique identifier for this setting /// - [defaultValue]: Initial floating-point value /// - [userConfigurable]: Whether users can modify this setting (default: true) /// - [validator]: Optional validation function (e.g., range checking) DoubleSetting({ required super.key, required super.defaultValue, super.userConfigurable, super.validator, }) : super(type: SettingType.double); } /// A setting that stores string text values. /// /// This is a concrete implementation of [Setting] specialized for text values. /// Ideal for names, URLs, file paths, themes, and textual preferences. /// /// Example: /// ```dart /// final theme = StringSetting( /// key: 'theme', /// defaultValue: 'light', /// validator: (value) => ['light', 'dark', 'auto'].contains(value), /// ); /// /// final serverUrl = StringSetting( /// key: 'serverUrl', /// defaultValue: 'https://api.example.com', /// validator: (value) => Uri.tryParse(value) != null, /// ); /// ``` class StringSetting extends Setting { /// Creates a new string setting. /// /// Parameters: /// - [key]: Unique identifier for this setting /// - [defaultValue]: Initial text value /// - [userConfigurable]: Whether users can modify this setting (default: true) /// - [validator]: Optional validation function (e.g., format checking) StringSetting({ required super.key, required super.defaultValue, super.userConfigurable, super.validator, }) : super(type: SettingType.string); }