Waaaay big enchancements

This commit is contained in:
Artem Kokos
2026-04-02 23:51:28 +07:00
parent 6221fbcc71
commit 5e09f41747
14 changed files with 1308 additions and 111 deletions

View File

@@ -1,4 +1,5 @@
import 'dart:async';
import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../models/home_config.dart';
@@ -90,9 +91,17 @@ class GroupsNotifier extends Notifier<List<dynamic>> {
/// чтобы UI не прыгал пока лампа ещё не ответила.
final Map<String, DateTime> _lockUntil = {};
/// Debounce-таймеры для слайдеров (яркость, темп, цвет)
final Map<String, Timer> _debounceTimers = {};
@override
List<dynamic> build() {
ref.onDispose(() => _timer?.cancel());
ref.onDispose(() {
_timer?.cancel();
for (final t in _debounceTimers.values) {
t.cancel();
}
});
return [];
}
@@ -140,16 +149,24 @@ class GroupsNotifier extends Notifier<List<dynamic>> {
if (resStatus.data != null &&
resStatus.data['results'] is List &&
resStatus.data['results'].isNotEmpty) {
final data = resStatus.data['results'][0]['status'];
map['last_state'] = {
'state': data['state'] == true,
'brightness': (data['dimming'] ?? 100).toInt(),
'temp': (data['temp'] ?? 4000).toInt(),
'r': (data['r'] ?? 0).toInt(),
'g': (data['g'] ?? 0).toInt(),
'b': (data['b'] ?? 0).toInt(),
'scene': data['scene'],
};
// Берём первый результат без ошибки, или просто первый
final results = resStatus.data['results'] as List;
final validResult = results.firstWhere(
(r) => r['status'] != null && r['error'] == null,
orElse: () => results.first,
);
final data = validResult['status'];
if (data != null) {
map['last_state'] = {
'state': data['state'] == true,
'brightness': (data['dimming'] ?? 100).toInt(),
'temp': (data['temp'] ?? 4000).toInt(),
'r': (data['r'] ?? 0).toInt(),
'g': (data['g'] ?? 0).toInt(),
'b': (data['b'] ?? 0).toInt(),
'scene': data['scene'],
};
}
}
} catch (e) {
// При ошибке опроса -- сохраняем предыдущее состояние
@@ -189,7 +206,30 @@ class GroupsNotifier extends Notifier<List<dynamic>> {
];
}
/// Включить/выключить группу
/// Debounce: отправить API-запрос с задержкой, но UI обновить сразу.
/// Если значение меняется быстро (слайдер тянут), отправляется только
/// последнее значение после паузы.
void _debouncedControl(String id, String key, Map<String, dynamic> localPatch,
Map<String, dynamic> apiParams) {
_setLock(id);
_updateLocal(id, localPatch);
final timerKey = '$id:$key';
_debounceTimers[timerKey]?.cancel();
_debounceTimers[timerKey] = Timer(
const Duration(milliseconds: 300),
() async {
try {
await _api.controlGroup(id, apiParams);
} catch (e) {
_lockUntil.remove(id);
refresh();
}
},
);
}
/// Включить/выключить группу (без debounce -- мгновенно)
Future<void> toggleGroup(String id, bool on) async {
_setLock(id);
_updateLocal(id, {'state': on});
@@ -201,32 +241,31 @@ class GroupsNotifier extends Notifier<List<dynamic>> {
}
}
/// Установить яркость (0-100)
Future<void> setBrightness(String id, int value) async {
_setLock(id);
_updateLocal(id, {'brightness': value});
await _api.controlGroup(id, {'brightness': value});
/// Установить яркость (0-100) -- с debounce
void setBrightness(String id, int value) {
_debouncedControl(id, 'brightness', {'brightness': value}, {'brightness': value});
}
/// Установить цветовую температуру (2700-6500K)
Future<void> setTemperature(String id, int value) async {
_setLock(id);
_updateLocal(id, {'temp': value});
await _api.controlGroup(id, {'temp': value});
/// Установить цветовую температуру (2700-6500K) -- с debounce
void setTemperature(String id, int value) {
_debouncedControl(id, 'temp', {'temp': value}, {'temp': value});
}
/// Установить RGB-цвет
Future<void> setColor(String id, int r, int g, int b) async {
_setLock(id);
_updateLocal(id, {'r': r, 'g': g, 'b': b});
await _api.controlGroup(id, {'r': r, 'g': g, 'b': b});
/// Установить RGB-цвет -- с debounce
void setColor(String id, int r, int g, int b) {
_debouncedControl(id, 'color', {'r': r, 'g': g, 'b': b}, {'r': r, 'g': g, 'b': b});
}
/// Установить сцену
/// Установить сцену (без debounce)
Future<void> setScene(String id, String scene) async {
_setLock(id);
_updateLocal(id, {'scene': scene});
await _api.controlGroup(id, {'scene': scene});
try {
await _api.controlGroup(id, {'scene': scene});
} catch (e) {
_lockUntil.remove(id);
refresh();
}
}
/// Таймер: включить на 4 часа
@@ -279,10 +318,22 @@ class ScenesNotifier extends Notifier<List<dynamic>> {
try {
final api = ref.read(apiProvider);
final res = await api.getScenes();
if (res.data is List) {
state = res.data;
} else if (res.data is Map) {
state = res.data['data'] ?? res.data['scenes'] ?? res.data.values.toList();
final data = res.data;
if (data is List) {
state = data;
} else if (data is Map) {
// Бэкенд может вернуть {scene_id: "Scene Name", ...}
// или {data: [...]} или {scenes: [...]}
if (data.containsKey('data') && data['data'] is List) {
state = data['data'];
} else if (data.containsKey('scenes') && data['scenes'] is List) {
state = data['scenes'];
} else {
// Map вида {id: name} -- преобразуем в список
state = data.entries
.map((e) => {'id': e.key.toString(), 'name': e.value.toString()})
.toList();
}
}
} catch (e) {
debugPrint("Ошибка загрузки сцен: $e");
@@ -303,10 +354,11 @@ class TasksNotifier extends Notifier<List<dynamic>> {
try {
final api = ref.read(apiProvider);
final res = await api.getTasks();
if (res.data is List) {
state = res.data;
} else if (res.data is Map) {
state = res.data['data'] ?? res.data['tasks'] ?? res.data.values.toList();
final data = res.data;
if (data is List) {
state = data;
} else if (data is Map) {
state = data['tasks'] ?? data['data'] ?? data.values.toList();
}
} catch (e) {
debugPrint("Ошибка загрузки расписаний: $e");
@@ -361,3 +413,162 @@ class TasksNotifier extends Notifier<List<dynamic>> {
await load();
}
}
// ─── Статистика ──────────────────────────────────────────────
final statsProvider =
NotifierProvider<StatsNotifier, Map<String, dynamic>>(() => StatsNotifier());
class StatsNotifier extends Notifier<Map<String, dynamic>> {
@override
Map<String, dynamic> build() => {};
Future<void> load({int days = 7}) async {
try {
final api = ref.read(apiProvider);
final res = await api.getStatsSummary(days: days);
final data = res.data;
if (data is Map) {
state = Map<String, dynamic>.from(data);
}
} catch (e) {
debugPrint("Ошибка загрузки статистики: $e");
}
}
}
// ─── Лог событий ─────────────────────────────────────────────
final eventLogProvider =
NotifierProvider<EventLogNotifier, List<dynamic>>(() => EventLogNotifier());
class EventLogNotifier extends Notifier<List<dynamic>> {
@override
List<dynamic> build() => [];
Future<void> load({int limit = 100}) async {
try {
final api = ref.read(apiProvider);
final res = await api.getStatsLog(limit: limit);
final data = res.data;
if (data is List) {
state = data;
} else if (data is Map) {
state = data['data'] ?? data['events'] ?? data.values.toList();
}
} catch (e) {
debugPrint("Ошибка загрузки логов: $e");
}
}
}
// ─── API-ключи ───────────────────────────────────────────────
final apiKeysProvider =
NotifierProvider<ApiKeysNotifier, List<dynamic>>(() => ApiKeysNotifier());
class ApiKeysNotifier extends Notifier<List<dynamic>> {
@override
List<dynamic> build() => [];
Future<void> load() async {
try {
final api = ref.read(apiProvider);
final res = await api.getApiKeys();
final data = res.data;
if (data is List) {
state = data;
} else if (data is Map) {
state = data['data'] ?? data['keys'] ?? data.values.toList();
}
} catch (e) {
debugPrint("Ошибка загрузки API-ключей: $e");
}
}
Future<String?> create(String name, {bool isAdmin = false}) async {
try {
final api = ref.read(apiProvider);
final res = await api.createApiKey(name, isAdmin: isAdmin);
await load();
if (res.data is Map) {
return res.data['key']?.toString();
}
return null;
} catch (e) {
debugPrint("Ошибка создания ключа: $e");
return null;
}
}
Future<void> revoke(String key) async {
try {
await ref.read(apiProvider).revokeApiKey(key);
await load();
} catch (e) {
debugPrint("Ошибка отзыва ключа: $e");
}
}
Future<void> activate(String key) async {
try {
await ref.read(apiProvider).activateApiKey(key);
await load();
} catch (e) {
debugPrint("Ошибка активации ключа: $e");
}
}
}
// ─── Информация об авторизации ────────────────────────────────
final authInfoProvider =
NotifierProvider<AuthInfoNotifier, Map<String, dynamic>?>(() => AuthInfoNotifier());
class AuthInfoNotifier extends Notifier<Map<String, dynamic>?> {
@override
Map<String, dynamic>? build() => null;
Future<void> load() async {
try {
final api = ref.read(apiProvider);
final res = await api.getAuthMe();
if (res.data is Map) {
state = Map<String, dynamic>.from(res.data);
}
} catch (e) {
debugPrint("Ошибка загрузки auth/me: $e");
}
}
bool get isAdmin => state?['is_admin'] == true;
}
// ─── Утилита: расчёт расстояния (Haversine) ──────────────────
double calculateDistanceKm(
double lat1, double lon1, double lat2, double lon2) {
const earthRadiusKm = 6371.0;
final dLat = _degToRad(lat2 - lat1);
final dLon = _degToRad(lon2 - lon1);
final a = math.sin(dLat / 2) * math.sin(dLat / 2) +
math.cos(_degToRad(lat1)) *
math.cos(_degToRad(lat2)) *
math.sin(dLon / 2) *
math.sin(dLon / 2);
final c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a));
return earthRadiusKm * c;
}
double _degToRad(double deg) => deg * (math.pi / 180);
/// Форматирование расстояния в человекочитаемый вид
String formatDistance(double km) {
if (km < 1.0) {
return '${(km * 1000).round()} м';
} else if (km < 10.0) {
return '${km.toStringAsFixed(1)} км';
} else {
return '${km.round()} км';
}
}