shitman/lib/settings/settings_manager.dart
zeyus bc128cef3d
Some checks are pending
/ build-web (push) Waiting to run
just a concept.
2025-07-21 22:52:31 +02:00

350 lines
12 KiB
Dart

import 'dart:async';
import 'settings_group.dart';
import 'exceptions.dart';
/// Global settings manager providing centralized access to all setting groups.
///
/// The [Settings] class serves as the main entry point for the settings framework,
/// offering static methods for registration, initialization, and access to settings
/// across your entire application. It manages multiple [SettingsGroup] instances and
/// provides both individual and batch operations.
///
/// ## Overview
///
/// The settings framework follows a hierarchical structure:
/// ```
/// Settings (Global Manager)
/// │
/// ├── SettingsGroup (Group: "game")
/// │ ├── BoolSetting ("soundEnabled")
/// │ └── DoubleSetting ("volume")
/// │
/// └── SettingsGroup (Group: "ui")
/// ├── StringSetting ("theme")
/// └── IntSetting ("fontSize")
/// ```
///
/// ## Usage Pattern
///
/// ```dart
/// // 1. Define your setting groups
/// final gameSettings = SettingsGroup(
/// key: 'game',
/// items: [
/// BoolSetting(key: 'soundEnabled', defaultValue: true),
/// DoubleSetting(key: 'volume', defaultValue: 0.8),
/// ],
/// );
///
/// final uiSettings = SettingsGroup(
/// key: 'ui',
/// items: [
/// StringSetting(key: 'theme', defaultValue: 'light'),
/// IntSetting(key: 'fontSize', defaultValue: 14),
/// ],
/// );
///
/// // 2. Register all groups
/// Settings.register(gameSettings);
/// Settings.register(uiSettings);
///
/// // 3. Initialize the entire settings system
/// await Settings.init();
///
/// // 4. Access settings using dot notation
/// bool soundEnabled = Settings.getBool('game.soundEnabled');
/// String theme = Settings.getString('ui.theme');
///
/// // 5. Modify settings with automatic validation
/// await Settings.setBool('game.soundEnabled', false);
/// await Settings.setString('ui.theme', 'dark');
///
/// // 6. Batch operations for efficiency
/// await Settings.setMultiple({
/// 'game.volume': 0.5,
/// 'ui.fontSize': 16,
/// });
///
/// // 7. Reset operations
/// await Settings.resetSetting('game.volume'); // Reset single setting
/// await Settings.resetGroup('ui'); // Reset entire group
/// await Settings.resetAll(); // Reset everything
/// ```
///
/// ## Storage Key Format
///
/// Settings are stored using a namespaced key format: `groupKey.settingKey`
/// - `game.soundEnabled` → boolean setting in the game group
/// - `ui.theme` → string setting in the ui group
/// - `network.timeout` → integer setting in the network group
///
/// This prevents key conflicts between different setting groups and provides
/// logical organization of related settings.
class Settings {
/// Internal registry of all settings groups keyed by their group names.
///
/// This map stores all registered [SettingsGroup] instances, providing
/// fast lookup by group key. Groups must be registered before use.
static final Map<String, SettingsGroup> _settings = {};
/// Initializes all registered settings groups concurrently.
///
/// This method waits for all registered settings groups to complete their
/// asynchronous initialization. It's essential to call this method before
/// accessing any setting values to ensure they've been loaded from storage.
///
/// The initialization process:
/// 1. Waits for the underlying SharedPreferences to be ready
/// 2. Loads existing values from storage for each setting
/// 3. Creates default values for settings that don't exist yet
/// 4. Marks all groups as ready for synchronous access
///
/// Returns: Future that completes when all settings are initialized
///
/// Throws: Exception if any settings group fails to initialize
///
/// Example:
/// ```dart
/// // Register your settings groups first
/// Settings.register(gameSettings);
/// Settings.register(uiSettings);
///
/// // Then initialize everything
/// await Settings.init();
///
/// // Now safe to use settings synchronously
/// bool soundEnabled = Settings.getBool('game.soundEnabled');
/// ```
Future<void> init() async {
final futures = _settings.values.map((settings) => settings.readyFuture);
await Future.wait(futures);
}
/// Returns a map of all registered settings groups.
Map<String, SettingsGroup> get groups => _settings;
/// Returns a list of all registered settings groups keys.
List<String> get groupKeys => _settings.keys.toList();
/// Allow access to settings by key using dynamic getters.
/// This allows you to access settings like:
/// Settings.game.fullscreen, Settings.game.soundVolume, etc.
@override
SettingsGroup noSuchMethod(Invocation invocation) {
if (invocation.isGetter) {
final key = invocation.memberName.toString();
if (_settings.containsKey(key)) {
return _settings[key]!;
}
}
throw NoSuchMethodError.withInvocation(this, invocation);
}
/// Validate and get the parts of a storage key.
/// The [storageKey] should be in the format "groupKey.settingKey".
/// Throws an [ArgumentError] if the storage key is invalid.
static ({String group, String setting}) _parseStorageKey(String storageKey) {
final parts = storageKey.split('.');
if (parts.length < 2) {
throw ArgumentError('Invalid storage key: $storageKey');
}
return (group: parts.first, setting: parts.sublist(1).join('.'));
}
// ===== Getters =====
/// Override the accessor to allow dynamic access to settings
/// using the `[]` operator.
dynamic operator [](String key) {
return get<dynamic>(key);
}
/// Registers a settings group with the global settings manager.
///
/// Each settings group must be registered before the system can be initialized.
/// Groups are identified by their unique key, and duplicate keys are not allowed.
///
/// This method should be called during application startup, before calling [init].
///
/// Parameters:
/// - [settings]: The SettingsGroup instance to register
///
/// Throws: [ArgumentError] if a group with the same key already exists
///
/// Example:
/// ```dart
/// final gameSettings = SettingsGroup(key: 'game', items: [...]);
/// final uiSettings = SettingsGroup(key: 'ui', items: [...]);
///
/// Settings.register(gameSettings);
/// Settings.register(uiSettings);
///
/// await Settings.init(); // Initialize after all groups are registered
/// ```
void register(SettingsGroup settings) {
if (_settings.containsKey(settings.key)) {
throw ArgumentError('Settings with key ${settings.key} already exists');
}
_settings[settings.key] = settings;
}
/// Gets a settings group by its key.
SettingsGroup getGroup(String key) {
if (!_settings.containsKey(key)) {
throw SettingNotFoundException('No settings group found for key: $key');
}
return _settings[key]!;
}
/// Get a setting by its storage key and type.
/// The [storageKey] should be in the format "groupKey.settingKey".
/// Throws an [ArgumentError] if the storage key is invalid or
/// if the setting is not found.
T get<T>(String storageKey) {
// Split the storage key to get the group key and setting key.
final id = _parseStorageKey(storageKey);
final group = getGroup(id.group);
return group.get<T>(id.setting);
}
// Helpers for typed access to settings.
// These methods are for convenience to access settings without
// ending up with a dynamic value.
/// Gets a boolean setting by its storage key.
/// The [storageKey] should be in the format "groupKey.settingKey".
/// Throws an [ArgumentError] if the storage key is invalid or
/// if the setting is not found or is not of type bool.
bool getBool(String storageKey) {
return get<bool>(storageKey);
}
/// Gets a double setting by its storage key.
/// The [storageKey] should be in the format "groupKey.settingKey".
/// Throws an [ArgumentError] if the storage key is invalid or
/// if the setting is not found or is not of type int.
int getInt(String storageKey) {
return get<int>(storageKey);
}
/// Gets a double setting by its storage key.
/// The [storageKey] should be in the format "groupKey.settingKey".
/// Throws an [ArgumentError] if the storage key is invalid or
/// if the setting is not found or is not of type double.
double getDouble(String storageKey) {
return get<double>(storageKey);
}
/// Gets a string setting by its storage key.
/// The [storageKey] should be in the format "groupKey.settingKey".
/// Throws an [ArgumentError] if the storage key is invalid or
/// if the setting is not found or is not of type string.
String getString(String storageKey) {
return get<String>(storageKey);
}
// ===== Setters =====
/// Sets a setting value by its storage key.
/// The [storageKey] should be in the format "groupKey.settingKey".
/// Throws an [ArgumentError] if the storage key is invalid or
/// if the setting is not found or is not user configurable.
Future<void> setValue(String storageKey, dynamic value) async {
final id = _parseStorageKey(storageKey);
final group = getGroup(id.group);
await group.setValue(id.setting, value);
}
/// Sets a setting value by its storage key and type.
/// The [storageKey] should be in the format "groupKey.settingKey".
/// Throws an [ArgumentError] if the storage key is invalid or
/// if the setting is not found or is not user configurable.
Future<void> set<T>(String storageKey, T value) async {
final id = _parseStorageKey(storageKey);
final group = getGroup(id.group);
await group.setValue<T>(id.setting, value);
}
/// Sets a boolean setting value by its storage key.
/// The [storageKey] should be in the format "groupKey.settingKey".
/// Throws an [ArgumentError] if the storage key is invalid or
/// if the setting is not found or is not user configurable.
Future<void> setBool(String storageKey, bool value) async {
await set<bool>(storageKey, value);
}
/// Sets an integer setting value by its storage key.
/// The [storageKey] should be in the format "groupKey.settingKey".
/// Throws an [ArgumentError] if the storage key is invalid or
/// if the setting is not found or is not user configurable.
Future<void> setInt(String storageKey, int value) async {
await set<int>(storageKey, value);
}
/// Sets a double setting value by its storage key.
/// The [storageKey] should be in the format "groupKey.settingKey".
/// Throws an [ArgumentError] if the storage key is invalid or
/// if the setting is not found or is not user configurable.
Future<void> setDouble(String storageKey, double value) async {
await set<double>(storageKey, value);
}
/// Sets a string setting value by its storage key.
/// The [storageKey] should be in the format "groupKey.settingKey".
/// Throws an [ArgumentError] if the storage key is invalid or
/// if the setting is not found or is not user configurable.
Future<void> setString(String storageKey, String value) async {
await set<String>(storageKey, value);
}
/// Sets multiple settings values in a batch operation.
/// The [settings] map should contain storage keys as keys and values as values.
/// This is more efficient than setting values individually.
Future<void> setMultiple(Map<String, dynamic> settings) async {
final futures = <Future<void>>[];
for (final entry in settings.entries) {
futures.add(setValue(entry.key, entry.value));
}
await Future.wait(futures);
}
/// Reset a setting to its default value by storage key.
/// The [storageKey] should be in the format "groupKey.settingKey".
Future<void> resetSetting(String storageKey) async {
final id = _parseStorageKey(storageKey);
final group = getGroup(id.group);
await group.reset(id.setting);
}
/// Reset all settings in a group to their default values.
/// The [groupKey] should be the key of the settings group.
Future<void> resetGroup(String groupKey) async {
final group = getGroup(groupKey);
await group.resetAll();
}
/// Reset all settings across all groups to their default values.
Future<void> resetAll() async {
final futures = _settings.values.map((group) => group.resetAll());
await Future.wait(futures);
}
/// Dispose all settings groups and their stream controllers.
void dispose() {
for (final group in _settings.values) {
group.dispose();
}
_settings.clear();
}
/// Clear all registered settings groups (for testing purposes).
void clearAll() {
dispose();
}
@override
String toString() {
return 'Settings{groups: ${_settings.keys.join(', ')}}';
}
}