Files
mirror-os-flutter/apps/remote/lib/screens/ha_entity_picker_screen.dart
T
2026-07-11 21:51:34 -05:00

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(),
);
},
),
),
],
);
}
}