class IgnisScene { final String id; final String displayName; const IgnisScene({required this.id, required this.displayName}); static IgnisScene fromApi(Object? value, {String? fallbackId}) { if (value is Map) { final map = Map.from(value); final id = _stringValue(map, const ['id', 'scene', 'scene_id', 'value']) ?? fallbackId; if (id == null || id.isEmpty) { throw const FormatException('scene не содержит id'); } final explicitName = _stringValue(map, const [ 'name', 'label', 'display_name', 'title', ]); return IgnisScene( id: id, displayName: explicitName ?? displayNameFor(id), ); } final id = value?.toString() ?? fallbackId; if (id == null || id.isEmpty) { throw const FormatException('scene должен быть объектом или id'); } return IgnisScene(id: id, displayName: displayNameFor(id)); } static List listFromApi(Object? data) { final values = _collectionValues(data, const ['data', 'scenes']); return values.map((value) { if (value.entryKey == null) { return IgnisScene.fromApi(value.value); } if (value.value is String || value.value is num) { final name = value.value.toString(); return IgnisScene( id: value.entryKey!, displayName: _looksLikeTechnicalId(name) ? displayNameFor(name) : name, ); } return IgnisScene.fromApi(value.value, fallbackId: value.entryKey); }).toList(); } static String displayNameFor(String id) { final normalized = id.trim(); final knownName = _wizSceneNames[normalized]; if (knownName != null) return knownName; return 'Сцена $normalized'; } } const Map _wizSceneNames = { '1': 'Океан', '2': 'Романтика', '3': 'Закат', '4': 'Вечеринка', '5': 'Камин', '6': 'Уют', '7': 'Лес', '8': 'Пастель', '9': 'Пробуждение', '10': 'Сон', '11': 'Тёплый белый', '12': 'Дневной свет', '13': 'Холодный белый', '14': 'Ночник', '15': 'Фокус', '16': 'Расслабление', '17': 'Настоящие цвета', '18': 'ТВ', '19': 'Рост растений', '20': 'Весна', '21': 'Лето', '22': 'Осень', '23': 'Погружение', '24': 'Джунгли', '25': 'Мохито', '26': 'Клуб', '27': 'Рождество', '28': 'Хэллоуин', '29': 'Свеча', '30': 'Золотистый белый', '31': 'Пульс', '32': 'Стимпанк', }; String? _stringValue(Map map, List keys) { for (final key in keys) { final value = map[key]; if (value != null && value.toString().isNotEmpty) { return value.toString(); } } return null; } bool _looksLikeTechnicalId(String value) => int.tryParse(value.trim()) != null; List<_CollectionValue> _collectionValues(Object? data, List wrappers) { if (data is List) { return data.map((value) => _CollectionValue(value)).toList(); } if (data is Map) { final map = Map.from(data); for (final wrapper in wrappers) { final value = map[wrapper]; if (value is List) { return value.map((item) => _CollectionValue(item)).toList(); } } return map.entries .map((entry) => _CollectionValue(entry.value, entryKey: entry.key)) .toList(); } throw const FormatException('ожидался список или объект'); } class _CollectionValue { final Object? value; final String? entryKey; const _CollectionValue(this.value, {this.entryKey}); }