Preserve Flutter mirror-os codebase
This commit is contained in:
@@ -0,0 +1,303 @@
|
||||
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),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user