Files
2026-07-11 21:51:34 -05:00

304 lines
9.6 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:mirror_core/mirror_core.dart';
import 'package:mirror_ui/mirror_ui.dart';
import 'package:mirror_protocol/mirror_protocol.dart';
import '../providers/connection_provider.dart';
import '../providers/ha_provider.dart';
import '../providers/update_provider.dart';
import 'settings_screen.dart';
class RemoteDashboard extends ConsumerWidget {
const RemoteDashboard({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final haState = ref.watch(haEntitiesProvider);
return SingleChildScrollView(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('Mirror OS', style: Theme.of(context).textTheme.headlineMedium),
IconButton(
icon: const Icon(Icons.settings),
tooltip: 'Settings',
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(builder: (_) => const SettingsScreen()),
);
},
),
],
),
const SizedBox(height: 16),
const UpdateBanner(),
const SizedBox(height: 16),
Text(
'Quick Actions',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: MirrorTheme.textMuted,
),
),
const SizedBox(height: 16),
_ActionCard(
icon: Icons.fitness_center,
title: 'Start Workout',
subtitle: 'Browse your video library',
color: MirrorTheme.accent,
onTap: () {
ref.read(connectionProvider.notifier).send(
NavigateCommand(target: ScreenType.workout),
);
ref.read(connectionProvider.notifier).send(
WorkoutCommand(
action: WorkoutAction.browse,
payload: {'path': '', 'filter': 'all'},
),
);
},
),
const SizedBox(height: 12),
_ActionCard(
icon: Icons.music_note,
title: 'Media',
subtitle: 'Play music or video',
color: MirrorTheme.success,
onTap: () {
ref.read(connectionProvider.notifier).send(
NavigateCommand(target: ScreenType.media),
);
},
),
const SizedBox(height: 12),
_ActionCard(
icon: Icons.nightlight_round,
title: 'Standby',
subtitle: 'Turn off the display',
color: MirrorTheme.textMuted,
onTap: () {
ref.read(connectionProvider.notifier).send(
NavigateCommand(target: ScreenType.standby),
);
},
),
// --- Home Assistant Device Controls ---
const SizedBox(height: 32),
Text(
'Home Assistant',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: MirrorTheme.textMuted,
),
),
const SizedBox(height: 16),
haState.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (e, _) => Text('HA Error: $e', style: TextStyle(color: MirrorTheme.danger, fontSize: 13)),
data: (data) {
if (data.error != null) {
return Container(
width: double.infinity,
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: MirrorTheme.surface,
borderRadius: BorderRadius.circular(12),
),
child: Column(
children: [
Icon(Icons.home, size: 36, color: MirrorTheme.textMuted),
const SizedBox(height: 8),
Text(data.error!, style: TextStyle(color: MirrorTheme.textMuted, fontSize: 13)),
],
),
);
}
return _HaDeviceControls(entities: data.entities);
},
),
// --- Scenes ---
const SizedBox(height: 24),
Text(
'Scenes',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: MirrorTheme.textMuted,
),
),
const SizedBox(height: 12),
_HaSceneButtons(),
],
),
);
}
}
// --- HA Device Controls ---
class _HaDeviceControls extends ConsumerWidget {
final List<HaEntity> entities;
const _HaDeviceControls({required this.entities});
@override
Widget build(BuildContext context, WidgetRef ref) {
final toggleable = entities.where((e) {
return e.entityId.startsWith('light.') || e.entityId.startsWith('switch.');
}).toList();
if (toggleable.isEmpty) {
return Container(
width: double.infinity,
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: MirrorTheme.surface,
borderRadius: BorderRadius.circular(12),
),
child: Column(
children: [
Icon(Icons.lightbulb_outline, size: 36, color: MirrorTheme.textMuted),
const SizedBox(height: 8),
Text(
'No lights or switches found',
style: TextStyle(color: MirrorTheme.textMuted, fontSize: 13),
),
],
),
);
}
return Column(
children: toggleable.map((entity) {
final friendlyName = entity.attributes['friendly_name'] as String? ??
entity.entityId.split('.').last;
final isOn = entity.state == 'on';
final isLight = entity.entityId.startsWith('light.');
return Container(
margin: const EdgeInsets.only(bottom: 8),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
color: MirrorTheme.surface,
borderRadius: BorderRadius.circular(12),
),
child: Row(
children: [
Icon(
isLight ? Icons.lightbulb : Icons.power_settings_new,
color: isOn ? MirrorTheme.accent : MirrorTheme.textMuted,
size: 24,
),
const SizedBox(width: 12),
Expanded(
child: Text(friendlyName, style: const TextStyle(fontSize: 14)),
),
Switch(
value: isOn,
onChanged: (_) {
ref.read(haEntitiesProvider.notifier).toggleEntity(entity.entityId);
},
),
],
),
);
}).toList(),
);
}
}
// --- Scene Buttons ---
class _HaSceneButtons extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final haState = ref.watch(haEntitiesProvider).valueOrNull;
if (haState == null || haState.error != null) return const SizedBox.shrink();
final scenes = haState.entities
.where((e) => e.entityId.startsWith('scene.'))
.toList();
if (scenes.isEmpty) return const SizedBox.shrink();
return Wrap(
spacing: 8,
runSpacing: 8,
children: scenes.map((scene) {
final name = scene.attributes['friendly_name'] as String? ??
scene.entityId.split('.').last.replaceAll('_', ' ');
return ActionChip(
avatar: const Icon(Icons.play_arrow, size: 18),
label: Text(name),
onPressed: () {
ref.read(haEntitiesProvider.notifier).callScene(scene.entityId);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Activating $name...')),
);
},
);
}).toList(),
);
}
}
class _ActionCard extends StatelessWidget {
final IconData icon;
final String title;
final String subtitle;
final Color color;
final VoidCallback onTap;
const _ActionCard({
required this.icon,
required this.title,
required this.subtitle,
required this.color,
required this.onTap,
});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: MirrorTheme.surface,
borderRadius: BorderRadius.circular(16),
),
child: Row(
children: [
Container(
width: 48,
height: 48,
decoration: BoxDecoration(
color: color.withValues(alpha: 0.15),
borderRadius: BorderRadius.circular(12),
),
child: Icon(icon, color: color),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title, style: Theme.of(context).textTheme.labelLarge),
const SizedBox(height: 2),
Text(
subtitle,
style: Theme.of(context).textTheme.bodySmall,
),
],
),
),
Icon(Icons.chevron_right, color: MirrorTheme.textMuted),
],
),
),
);
}
}