Waaaay big enchancements
This commit is contained in:
@@ -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()} км';
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user