136 lines
4.3 KiB
Dart
136 lines
4.3 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:mirror_ui/mirror_ui.dart';
|
|
import '../providers/ha_provider.dart';
|
|
|
|
class HaEntityPickerScreen extends ConsumerStatefulWidget {
|
|
final List<String> selectedIds;
|
|
|
|
const HaEntityPickerScreen({super.key, this.selectedIds = const []});
|
|
|
|
@override
|
|
ConsumerState<HaEntityPickerScreen> createState() => _HaEntityPickerScreenState();
|
|
}
|
|
|
|
class _HaEntityPickerScreenState extends ConsumerState<HaEntityPickerScreen> {
|
|
late Set<String> _selected;
|
|
String _search = '';
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_selected = Set.from(widget.selectedIds);
|
|
}
|
|
|
|
String _domainOf(String entityId) {
|
|
final dotIndex = entityId.indexOf('.');
|
|
return dotIndex > 0 ? entityId.substring(0, dotIndex) : 'other';
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final entitiesAsync = ref.watch(haEntitiesProvider);
|
|
|
|
return Scaffold(
|
|
appBar: AppBar(
|
|
title: const Text('Select HA Entities'),
|
|
backgroundColor: MirrorTheme.background,
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.of(context).pop(_selected.toList()),
|
|
child: Text('Done (${_selected.length})'),
|
|
),
|
|
],
|
|
),
|
|
backgroundColor: MirrorTheme.background,
|
|
body: entitiesAsync.when(
|
|
loading: () => const Center(child: CircularProgressIndicator()),
|
|
error: (e, _) => Center(child: Text('Error: $e')),
|
|
data: (state) {
|
|
if (state.error != null) {
|
|
return Center(child: Text(state.error!));
|
|
}
|
|
return _buildList(state.entities);
|
|
},
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildList(List entities) {
|
|
// Filter by search
|
|
final filtered = entities.where((e) {
|
|
final id = e.entityId as String;
|
|
final name =
|
|
(e.attributes['friendly_name'] as String?) ?? id;
|
|
final query = _search.toLowerCase();
|
|
return id.toLowerCase().contains(query) || name.toLowerCase().contains(query);
|
|
}).toList();
|
|
|
|
// Group by domain
|
|
final grouped = <String, List<dynamic>>{};
|
|
for (final entity in filtered) {
|
|
final domain = _domainOf(entity.entityId as String);
|
|
grouped.putIfAbsent(domain, () => []).add(entity);
|
|
}
|
|
|
|
final sortedDomains = grouped.keys.toList()..sort();
|
|
|
|
return Column(
|
|
children: [
|
|
Padding(
|
|
padding: const EdgeInsets.all(12),
|
|
child: TextField(
|
|
decoration: InputDecoration(
|
|
hintText: 'Search entities...',
|
|
prefixIcon: const Icon(Icons.search),
|
|
filled: true,
|
|
fillColor: MirrorTheme.surface,
|
|
border: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
borderSide: BorderSide.none,
|
|
),
|
|
),
|
|
onChanged: (v) => setState(() => _search = v),
|
|
),
|
|
),
|
|
Expanded(
|
|
child: ListView.builder(
|
|
itemCount: sortedDomains.length,
|
|
itemBuilder: (context, domainIndex) {
|
|
final domain = sortedDomains[domainIndex];
|
|
final items = grouped[domain]!;
|
|
return ExpansionTile(
|
|
title: Text(
|
|
domain,
|
|
style: TextStyle(color: MirrorTheme.accent, fontWeight: FontWeight.w600),
|
|
),
|
|
initiallyExpanded: items.any((e) => _selected.contains(e.entityId)),
|
|
children: items.map((entity) {
|
|
final id = entity.entityId as String;
|
|
final name =
|
|
(entity.attributes['friendly_name'] as String?) ?? id;
|
|
return CheckboxListTile(
|
|
title: Text(name),
|
|
subtitle: Text(id, style: const TextStyle(fontSize: 12)),
|
|
value: _selected.contains(id),
|
|
activeColor: MirrorTheme.accent,
|
|
onChanged: (checked) {
|
|
setState(() {
|
|
if (checked == true) {
|
|
_selected.add(id);
|
|
} else {
|
|
_selected.remove(id);
|
|
}
|
|
});
|
|
},
|
|
);
|
|
}).toList(),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|