343 lines
10 KiB
Dart
343 lines
10 KiB
Dart
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<T> implements Serializable {
|
|
/// Internal stream controller for broadcasting value changes.
|
|
/// Uses broadcast to allow multiple listeners.
|
|
final StreamController<T> _controller = StreamController<T>.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<T> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic>);
|
|
}
|
|
|
|
/// 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<bool> {
|
|
/// 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<int> {
|
|
/// 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<double> {
|
|
/// 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<String> {
|
|
/// 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);
|
|
}
|