Files
ignis_app/lib/screens/wiz_provisioning_screen.dart
2026-05-16 17:24:28 +07:00

532 lines
20 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../features/provisioning/models/wiz_provisioning_environment.dart';
import '../features/provisioning/models/wiz_provisioning_state.dart';
import '../features/provisioning/providers/wiz_provisioning_providers.dart';
class WizProvisioningScreen extends ConsumerStatefulWidget {
const WizProvisioningScreen({super.key});
@override
ConsumerState<WizProvisioningScreen> createState() =>
_WizProvisioningScreenState();
}
class _WizProvisioningScreenState extends ConsumerState<WizProvisioningScreen>
with WidgetsBindingObserver {
final _formKey = GlobalKey<FormState>();
final _ssidCtrl = TextEditingController();
final _bssidCtrl = TextEditingController();
final _passwordCtrl = TextEditingController();
bool _ssidTouched = false;
bool _bssidTouched = false;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
Future<void>.microtask(
() => ref.read(wizProvisioningProvider.notifier).initialize(),
);
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
ref
.read(wizProvisioningProvider.notifier)
.cancelProvisioning(keepCurrentState: false);
ref.invalidate(wizProvisioningProvider);
_ssidCtrl.dispose();
_bssidCtrl.dispose();
_passwordCtrl.dispose();
super.dispose();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.resumed) {
Future<void>.microtask(
() => ref.read(wizProvisioningProvider.notifier).initialize(),
);
}
}
@override
Widget build(BuildContext context) {
final provisioningState = ref.watch(wizProvisioningProvider);
_syncControllers(provisioningState.environment);
final bottomInset = MediaQuery.paddingOf(context).bottom;
final environment = provisioningState.environment;
final failure = provisioningState.failure;
final canRequestPermissions =
!environment.permissionsGranted && environment.permissionRequestable;
final canOpenAppSettings =
environment.requiresAppSettings && environment.appSettingsSupported;
final needsWifiSettings =
!environment.connectedToWifi && environment.wifiSettingsSupported;
return Scaffold(
appBar: AppBar(title: const Text('ПОДКЛЮЧЕНИЕ WIZ')),
body: SafeArea(
top: false,
bottom: true,
child: RefreshIndicator(
color: Colors.deepOrange,
onRefresh: () =>
ref.read(wizProvisioningProvider.notifier).initialize(),
child: ListView(
padding: EdgeInsets.fromLTRB(16, 16, 16, bottomInset + 24),
children: [
_SectionCard(
title: 'Что делает мастер',
child: const Text(
'Эта версия использует smart pairing: телефон остаётся в домашней Wi-Fi сети и передаёт её настройки новой лампе. Это Android-only поток и он лучше всего работает, когда телефон уже сидит на 2.4 GHz.',
style: TextStyle(color: Colors.white70),
),
),
_SectionCard(
title: 'Активный дом',
child: Text(
provisioningState.activeHomeName == null
? 'Не выбран'
: provisioningState.activeHomeName!,
style: TextStyle(
color: provisioningState.activeHomeName == null
? Colors.redAccent
: Colors.white,
fontWeight: FontWeight.w600,
),
),
),
_SectionCard(
title: 'Окружение',
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_InfoRow(
label: 'Платформа',
value: environment.isAndroid
? 'Android ${environment.androidApiLevel ?? '?'}'
: environment.platform,
),
_InfoRow(
label: 'Разрешения',
value: _permissionStatusLabel(
environment.permissionStatus,
),
),
_InfoRow(
label: 'Wi-Fi',
value: environment.connectedToWifi
? (environment.ssid ?? 'Подключено')
: 'Нет подключения',
),
_InfoRow(
label: 'BSSID',
value: environment.bssid ?? 'Не удалось определить',
),
_InfoRow(
label: 'Диапазон',
value: environment.frequencyMhz == null
? 'Неизвестно'
: '${environment.frequencyMhz} MHz',
),
if (environment.isLikelyOn5Ghz)
const Padding(
padding: EdgeInsets.only(top: 8),
child: Text(
'Сейчас похоже активен 5 GHz. Для WiZ лучше заранее переключиться на 2.4 GHz.',
style: TextStyle(color: Colors.amberAccent),
),
),
if (!environment.locationServicesEnabled)
const Padding(
padding: EdgeInsets.only(top: 8),
child: Text(
'На Android системная геолокация должна быть включена, иначе SSID/BSSID часто скрываются системой.',
style: TextStyle(color: Colors.amberAccent),
),
),
],
),
),
if (failure != null)
_SectionCard(
title: 'Проблема',
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
failure.message,
style: const TextStyle(color: Colors.redAccent),
),
if (failure.details != null) ...[
const SizedBox(height: 8),
Text(
failure.details!,
style: const TextStyle(color: Colors.white54),
),
],
],
),
),
if (provisioningState.notice != null)
_SectionCard(
title: 'Примечание',
child: Text(
provisioningState.notice!,
style: const TextStyle(color: Colors.white70),
),
),
_SectionCard(
title: 'Шаги перед стартом',
child: const Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'1. Убедитесь, что телефон подключён к домашней 2.4 GHz Wi-Fi.',
style: TextStyle(color: Colors.white70),
),
SizedBox(height: 6),
Text(
'2. Переведите лампу в pairing mode: если нужно, несколько раз выключите и включите питание до пульсации.',
style: TextStyle(color: Colors.white70),
),
SizedBox(height: 6),
Text(
'3. Держите телефон рядом с лампой и не сворачивайте приложение до конца pairing.',
style: TextStyle(color: Colors.white70),
),
],
),
),
_SectionCard(
title: 'Домашняя Wi-Fi',
child: Form(
key: _formKey,
autovalidateMode: AutovalidateMode.onUserInteraction,
child: Column(
children: [
TextFormField(
controller: _ssidCtrl,
decoration: const InputDecoration(
labelText: 'SSID',
hintText: 'Например: Home-2G',
prefixIcon: Icon(Icons.wifi),
),
onChanged: (_) => _ssidTouched = true,
validator: (value) {
if ((value?.trim().isEmpty ?? true)) {
return 'Укажите SSID';
}
return null;
},
),
const SizedBox(height: 12),
TextFormField(
controller: _bssidCtrl,
decoration: const InputDecoration(
labelText: 'BSSID (опционально)',
hintText: 'aa:bb:cc:dd:ee:ff',
prefixIcon: Icon(Icons.router_outlined),
),
onChanged: (_) => _bssidTouched = true,
),
const SizedBox(height: 12),
TextFormField(
controller: _passwordCtrl,
decoration: const InputDecoration(
labelText: 'Пароль Wi-Fi',
hintText: 'Оставьте пустым для открытой сети',
prefixIcon: Icon(Icons.key),
),
obscureText: true,
),
],
),
),
),
_ActionSection(
state: provisioningState,
canRequestPermissions: canRequestPermissions,
canOpenAppSettings: canOpenAppSettings,
needsWifiSettings: needsWifiSettings,
onRequestPermissions: () => ref
.read(wizProvisioningProvider.notifier)
.requestPermissions(),
onOpenAppSettings: () => ref
.read(wizProvisioningProvider.notifier)
.openAppSettings(),
onOpenWifiSettings: () => ref
.read(wizProvisioningProvider.notifier)
.openWifiSettings(),
onRefresh: () =>
ref.read(wizProvisioningProvider.notifier).initialize(),
onStart: _startProvisioning,
onCancel: () => ref
.read(wizProvisioningProvider.notifier)
.cancelProvisioning(),
),
if (provisioningState.provisionedDevices.isNotEmpty)
_SectionCard(
title: 'Ответившие устройства',
child: Column(
children: [
for (final device in provisioningState.provisionedDevices)
ListTile(
dense: true,
contentPadding: EdgeInsets.zero,
leading: const Icon(
Icons.lightbulb_outline,
color: Colors.deepOrange,
),
title: Text(device.bssid),
subtitle: device.ipAddress == null
? null
: Text(device.ipAddress!),
),
],
),
),
if (provisioningState.rescanSummary != null)
_SectionCard(
title: 'Результат discovery',
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_InfoRow(
label: 'Найдено',
value: '${provisioningState.rescanSummary!.found}',
),
_InfoRow(
label: 'Добавлено',
value: '${provisioningState.rescanSummary!.added}',
),
_InfoRow(
label: 'Обновлено',
value: '${provisioningState.rescanSummary!.updated}',
),
_InfoRow(
label: 'Онлайн',
value: '${provisioningState.rescanSummary!.online}',
),
],
),
),
if (provisioningState.timeline.isNotEmpty)
_SectionCard(
title: 'Ход выполнения',
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
for (final event in provisioningState.timeline)
Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Text(
'$event',
style: const TextStyle(color: Colors.white60),
),
),
],
),
),
],
),
),
),
);
}
void _syncControllers(WizProvisioningEnvironment environment) {
if (!_ssidTouched &&
environment.ssid != null &&
environment.ssid != _ssidCtrl.text) {
_ssidCtrl.text = environment.ssid!;
}
if (!_bssidTouched &&
environment.bssid != null &&
environment.bssid != _bssidCtrl.text) {
_bssidCtrl.text = environment.bssid!;
}
}
Future<void> _startProvisioning() async {
if (!_formKey.currentState!.validate()) {
return;
}
await ref
.read(wizProvisioningProvider.notifier)
.startProvisioning(
ssid: _ssidCtrl.text,
password: _passwordCtrl.text,
bssid: _bssidCtrl.text.trim().isEmpty ? null : _bssidCtrl.text,
);
}
String _permissionStatusLabel(WizProvisioningPermissionStatus status) {
switch (status) {
case WizProvisioningPermissionStatus.granted:
return 'Выданы';
case WizProvisioningPermissionStatus.requestable:
return 'Нужно запросить';
case WizProvisioningPermissionStatus.settingsRequired:
return 'Нужно открыть настройки приложения';
}
}
}
class _ActionSection extends StatelessWidget {
final WizProvisioningState state;
final bool canRequestPermissions;
final bool canOpenAppSettings;
final bool needsWifiSettings;
final VoidCallback onRequestPermissions;
final VoidCallback onOpenAppSettings;
final VoidCallback onOpenWifiSettings;
final VoidCallback onRefresh;
final VoidCallback onStart;
final VoidCallback onCancel;
const _ActionSection({
required this.state,
required this.canRequestPermissions,
required this.canOpenAppSettings,
required this.needsWifiSettings,
required this.onRequestPermissions,
required this.onOpenAppSettings,
required this.onOpenWifiSettings,
required this.onRefresh,
required this.onStart,
required this.onCancel,
});
@override
Widget build(BuildContext context) {
final canStartProvisioning =
!state.isBusy &&
state.activeHomeName != null &&
state.environment.permissionsGranted &&
state.environment.locationServicesEnabled &&
state.environment.connectedToWifi &&
state.status != WizProvisioningStatus.unsupported;
return _SectionCard(
title: 'Действия',
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
if (state.isBusy)
const Padding(
padding: EdgeInsets.only(bottom: 12),
child: LinearProgressIndicator(color: Colors.deepOrange),
),
FilledButton.icon(
onPressed: canStartProvisioning ? onStart : null,
icon: const Icon(Icons.flash_on),
label: Text(
state.status == WizProvisioningStatus.success
? 'Повторить pairing'
: 'Запустить smart pairing',
),
),
const SizedBox(height: 8),
OutlinedButton.icon(
onPressed: state.isBusy ? onCancel : onRefresh,
icon: Icon(
state.isBusy ? Icons.stop_circle_outlined : Icons.refresh,
),
label: Text(state.isBusy ? 'Остановить' : 'Переобновить окружение'),
),
if (canRequestPermissions) ...[
const SizedBox(height: 8),
OutlinedButton.icon(
onPressed: state.isBusy ? null : onRequestPermissions,
icon: const Icon(Icons.privacy_tip_outlined),
label: const Text('Выдать разрешения'),
),
],
if (canOpenAppSettings) ...[
const SizedBox(height: 8),
OutlinedButton.icon(
onPressed: state.isBusy ? null : onOpenAppSettings,
icon: const Icon(Icons.settings_applications_outlined),
label: const Text('Открыть настройки приложения'),
),
],
if (needsWifiSettings) ...[
const SizedBox(height: 8),
OutlinedButton.icon(
onPressed: state.isBusy ? null : onOpenWifiSettings,
icon: const Icon(Icons.wifi_find_outlined),
label: const Text('Открыть настройки Wi-Fi'),
),
],
],
),
);
}
}
class _SectionCard extends StatelessWidget {
final String title;
final Widget child;
const _SectionCard({required this.title, required this.child});
@override
Widget build(BuildContext context) {
return Card(
margin: const EdgeInsets.only(bottom: 12),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: const TextStyle(
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
const SizedBox(height: 12),
child,
],
),
),
);
}
}
class _InfoRow extends StatelessWidget {
final String label;
final String value;
const _InfoRow({required this.label, required this.value});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 112,
child: Text(label, style: const TextStyle(color: Colors.white38)),
),
Expanded(
child: Text(value, style: const TextStyle(color: Colors.white70)),
),
],
),
);
}
}