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

205 lines
7.0 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:mirror_protocol/mirror_protocol.dart';
import 'package:mirror_ui/mirror_ui.dart';
import '../providers/connection_provider.dart';
import 'ha_entity_picker_screen.dart';
import 'weather_picker_screen.dart';
class DashboardEditorScreen extends ConsumerStatefulWidget {
const DashboardEditorScreen({super.key});
@override
ConsumerState<DashboardEditorScreen> createState() => _DashboardEditorScreenState();
}
class _DashboardEditorScreenState extends ConsumerState<DashboardEditorScreen> {
List<DashboardWidgetConfig> _widgets = [];
bool _loading = true;
static const _widgetMeta = <String, ({IconData icon, String label})>{
'clock': (icon: Icons.access_time, label: 'Clock'),
'weather': (icon: Icons.cloud, label: 'Weather'),
'calendar': (icon: Icons.calendar_month, label: 'Calendar'),
'ha_entities': (icon: Icons.home, label: 'HA Entities'),
};
@override
void initState() {
super.initState();
_requestConfig();
}
void _requestConfig() {
final existing = ref.read(dashboardConfigProvider);
if (existing != null) {
setState(() {
_widgets = List.from(existing.widgets);
_loading = false;
});
} else {
// Request from display
ref.read(connectionProvider.notifier).send(DashboardConfigRequest());
// Wait briefly then fall back to default
Future.delayed(const Duration(seconds: 2), () {
if (_loading && mounted) {
setState(() {
_widgets = List.from(DashboardLayoutConfig.defaultLayout().widgets);
_loading = false;
});
}
});
}
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
// Listen for config response
ref.listen(dashboardConfigProvider, (prev, next) {
if (next != null && _loading) {
setState(() {
_widgets = List.from(next.widgets);
_loading = false;
});
}
});
}
void _onReorder(int oldIndex, int newIndex) {
setState(() {
if (newIndex > oldIndex) newIndex--;
final item = _widgets.removeAt(oldIndex);
_widgets.insert(newIndex, item);
// Update positions
for (var i = 0; i < _widgets.length; i++) {
_widgets[i] = _widgets[i].copyWith(position: i);
}
});
}
void _toggleVisibility(int index) {
setState(() {
_widgets[index] = _widgets[index].copyWith(visible: !_widgets[index].visible);
});
}
Future<void> _openConfig(int index) async {
final widget = _widgets[index];
switch (widget.type) {
case 'ha_entities':
final currentIds =
(widget.config['entity_ids'] as List<dynamic>?)?.cast<String>() ?? [];
final result = await Navigator.of(context).push<List<String>>(
MaterialPageRoute(
builder: (_) => HaEntityPickerScreen(selectedIds: currentIds),
),
);
if (result != null && mounted) {
setState(() {
_widgets[index] = widget.copyWith(
config: {...widget.config, 'entity_ids': result},
);
});
}
case 'weather':
final currentEntity = widget.config['entity_id'] as String?;
final result = await Navigator.of(context).push<String>(
MaterialPageRoute(
builder: (_) => WeatherPickerScreen(selectedEntityId: currentEntity),
),
);
if (result != null && mounted) {
setState(() {
_widgets[index] = widget.copyWith(
config: {...widget.config, 'entity_id': result},
);
});
}
default:
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('No settings for ${widget.type}')),
);
}
}
void _saveLayout() {
final layout = DashboardLayoutConfig(widgets: _widgets);
ref.read(connectionProvider.notifier).send(DashboardConfigUpdate(layout: layout));
ref.read(dashboardConfigProvider.notifier).state = layout;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Dashboard layout saved')),
);
Navigator.of(context).pop();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Dashboard Layout'),
backgroundColor: MirrorTheme.background,
),
backgroundColor: MirrorTheme.background,
body: _loading
? const Center(child: CircularProgressIndicator())
: Column(
children: [
Expanded(
child: ReorderableListView.builder(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
itemCount: _widgets.length,
onReorderItem: (oldIndex, newIndex) => _onReorder(oldIndex, newIndex),
itemBuilder: (context, index) {
final widget = _widgets[index];
final meta = _widgetMeta[widget.type];
final icon = meta?.icon ?? Icons.widgets;
final label = meta?.label ?? widget.type;
final hasConfig =
widget.type == 'ha_entities' || widget.type == 'weather';
return Card(
key: ValueKey(widget.id),
color: MirrorTheme.surface,
margin: const EdgeInsets.symmetric(vertical: 4),
child: ListTile(
leading: Icon(icon, color: MirrorTheme.accent),
title: Text(label),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (hasConfig)
IconButton(
icon: const Icon(Icons.settings, size: 20),
onPressed: () => _openConfig(index),
),
Switch(
value: widget.visible,
onChanged: (_) => _toggleVisibility(index),
activeThumbColor: MirrorTheme.accent,
),
const Icon(Icons.drag_handle),
],
),
),
);
},
),
),
Padding(
padding: const EdgeInsets.all(16),
child: SizedBox(
width: double.infinity,
child: FilledButton.icon(
onPressed: _saveLayout,
icon: const Icon(Icons.save),
label: const Text('Save Layout'),
),
),
),
],
),
);
}
}