304 lines
9.6 KiB
Dart
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),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|