368 lines
11 KiB
Dart
368 lines
11 KiB
Dart
/// 自然写互动课堂平板端应用软件 V1.0
|
|
/// 护眼管理器 - 色温调节、使用时长监控、距离检测
|
|
///
|
|
/// 功能说明:
|
|
/// 1. 色温调节(暖色滤镜,减少蓝光对眼睛的刺激)
|
|
/// 2. 使用时长监控(按应用/科目统计,超时提醒休息)
|
|
/// 3. 距离检测(前置摄像头检测用眼距离,过近时提醒)
|
|
/// 4. 定时提醒(每30分钟提醒休息,远眺放松)
|
|
/// 5. 家长远程管控(接收家长设置的时段/时长限制)
|
|
/// 6. 护眼数据统计(每日使用时长报告)
|
|
|
|
import 'dart:async';
|
|
|
|
/// 护眼模式配置
|
|
class EyeCareConfig {
|
|
/// 是否启用护眼模式
|
|
bool enabled;
|
|
|
|
/// 色温强度(0.0=关闭, 1.0=最暖)
|
|
double colorTemperature;
|
|
|
|
/// 连续使用提醒间隔(分钟)
|
|
int reminderIntervalMinutes;
|
|
|
|
/// 每日使用时长上限(分钟,0=不限制)
|
|
int dailyLimitMinutes;
|
|
|
|
/// 允许使用的时段(开始小时, 结束小时)
|
|
int allowedStartHour;
|
|
int allowedEndHour;
|
|
|
|
/// 是否启用距离检测
|
|
bool distanceDetectionEnabled;
|
|
|
|
/// 安全用眼距离(厘米)
|
|
int safeDistanceCm;
|
|
|
|
/// 夜间模式自动开启时间(小时)
|
|
int nightModeStartHour;
|
|
int nightModeEndHour;
|
|
|
|
EyeCareConfig({
|
|
this.enabled = true,
|
|
this.colorTemperature = 0.3,
|
|
this.reminderIntervalMinutes = 30,
|
|
this.dailyLimitMinutes = 120,
|
|
this.allowedStartHour = 7,
|
|
this.allowedEndHour = 21,
|
|
this.distanceDetectionEnabled = false,
|
|
this.safeDistanceCm = 30,
|
|
this.nightModeStartHour = 20,
|
|
this.nightModeEndHour = 7,
|
|
});
|
|
|
|
Map<String, dynamic> toJson() => {
|
|
'enabled': enabled,
|
|
'color_temperature': colorTemperature,
|
|
'reminder_interval': reminderIntervalMinutes,
|
|
'daily_limit': dailyLimitMinutes,
|
|
'allowed_start': allowedStartHour,
|
|
'allowed_end': allowedEndHour,
|
|
'distance_enabled': distanceDetectionEnabled,
|
|
'safe_distance': safeDistanceCm,
|
|
'night_start': nightModeStartHour,
|
|
'night_end': nightModeEndHour,
|
|
};
|
|
|
|
factory EyeCareConfig.fromJson(Map<String, dynamic> json) {
|
|
return EyeCareConfig(
|
|
enabled: json['enabled'] ?? true,
|
|
colorTemperature: (json['color_temperature'] ?? 0.3).toDouble(),
|
|
reminderIntervalMinutes: json['reminder_interval'] ?? 30,
|
|
dailyLimitMinutes: json['daily_limit'] ?? 120,
|
|
allowedStartHour: json['allowed_start'] ?? 7,
|
|
allowedEndHour: json['allowed_end'] ?? 21,
|
|
distanceDetectionEnabled: json['distance_enabled'] ?? false,
|
|
safeDistanceCm: json['safe_distance'] ?? 30,
|
|
nightModeStartHour: json['night_start'] ?? 20,
|
|
nightModeEndHour: json['night_end'] ?? 7,
|
|
);
|
|
}
|
|
}
|
|
|
|
/// 使用时长记录
|
|
class UsageRecord {
|
|
final String date; // 日期 (yyyy-MM-dd)
|
|
final String category; // 分类 (homework/practice/reading)
|
|
final int durationMinutes; // 使用时长(分钟)
|
|
final int sessionCount; // 使用次数
|
|
|
|
UsageRecord({
|
|
required this.date,
|
|
required this.category,
|
|
required this.durationMinutes,
|
|
required this.sessionCount,
|
|
});
|
|
|
|
Map<String, dynamic> toJson() => {
|
|
'date': date, 'category': category,
|
|
'duration': durationMinutes, 'sessions': sessionCount,
|
|
};
|
|
}
|
|
|
|
/// 护眼事件类型
|
|
enum EyeCareEvent {
|
|
restReminder, // 休息提醒
|
|
dailyLimitReached, // 每日时长上限
|
|
outsideAllowedTime, // 超出允许使用时段
|
|
tooCloseWarning, // 用眼距离过近
|
|
nightModeOn, // 夜间模式开启
|
|
nightModeOff, // 夜间模式关闭
|
|
}
|
|
|
|
/// 护眼事件回调
|
|
typedef EyeCareEventCallback = void Function(EyeCareEvent event, Map<String, dynamic> data);
|
|
|
|
/// 护眼管理器
|
|
class EyeCareManager {
|
|
/// 护眼配置
|
|
EyeCareConfig _config = EyeCareConfig();
|
|
|
|
/// 事件回调列表
|
|
final List<EyeCareEventCallback> _callbacks = [];
|
|
|
|
/// 当前会话开始时间
|
|
DateTime? _sessionStartTime;
|
|
|
|
/// 今日累计使用时长(秒)
|
|
int _todayUsageSeconds = 0;
|
|
|
|
/// 当前连续使用时长(秒)
|
|
int _continuousUsageSeconds = 0;
|
|
|
|
/// 今日使用记录
|
|
final Map<String, int> _categoryUsage = {};
|
|
|
|
/// 计时器(每秒更新使用时长)
|
|
Timer? _usageTimer;
|
|
|
|
/// 距离检测计时器
|
|
Timer? _distanceTimer;
|
|
|
|
/// 夜间模式检查计时器
|
|
Timer? _nightModeTimer;
|
|
|
|
/// 当前是否在夜间模式
|
|
bool _isNightMode = false;
|
|
|
|
/// 当前色温值(供外部读取)
|
|
double get currentColorTemperature {
|
|
if (!_config.enabled) return 0.0;
|
|
if (_isNightMode) return _config.colorTemperature * 1.5; // 夜间加强
|
|
return _config.colorTemperature;
|
|
}
|
|
|
|
/// 今日总使用时长(分钟)
|
|
int get todayUsageMinutes => _todayUsageSeconds ~/ 60;
|
|
|
|
/// 剩余可用时长(分钟,-1表示不限制)
|
|
int get remainingMinutes {
|
|
if (_config.dailyLimitMinutes <= 0) return -1;
|
|
return _config.dailyLimitMinutes - todayUsageMinutes;
|
|
}
|
|
|
|
/// 注册事件回调
|
|
void addCallback(EyeCareEventCallback callback) {
|
|
_callbacks.add(callback);
|
|
}
|
|
|
|
/// 移除事件回调
|
|
void removeCallback(EyeCareEventCallback callback) {
|
|
_callbacks.remove(callback);
|
|
}
|
|
|
|
/// 更新配置(家长远程设置后调用)
|
|
void updateConfig(EyeCareConfig newConfig) {
|
|
_config = newConfig;
|
|
if (_config.enabled) {
|
|
_startMonitoring();
|
|
} else {
|
|
_stopMonitoring();
|
|
}
|
|
}
|
|
|
|
/// 开始使用(进入学习功能时调用)
|
|
void startSession({String category = 'default'}) {
|
|
_sessionStartTime = DateTime.now();
|
|
_continuousUsageSeconds = 0;
|
|
|
|
// 检查是否在允许时段内
|
|
final now = DateTime.now();
|
|
if (_config.enabled && !_isWithinAllowedTime(now)) {
|
|
_notifyEvent(EyeCareEvent.outsideAllowedTime, {
|
|
'allowed_start': _config.allowedStartHour,
|
|
'allowed_end': _config.allowedEndHour,
|
|
});
|
|
}
|
|
|
|
// 启动使用时长计时器
|
|
_usageTimer?.cancel();
|
|
_usageTimer = Timer.periodic(const Duration(seconds: 1), (_) {
|
|
_todayUsageSeconds++;
|
|
_continuousUsageSeconds++;
|
|
|
|
// 检查连续使用时长提醒
|
|
if (_config.reminderIntervalMinutes > 0 &&
|
|
_continuousUsageSeconds > 0 &&
|
|
_continuousUsageSeconds % (_config.reminderIntervalMinutes * 60) == 0) {
|
|
_notifyEvent(EyeCareEvent.restReminder, {
|
|
'continuous_minutes': _continuousUsageSeconds ~/ 60,
|
|
'total_minutes': todayUsageMinutes,
|
|
});
|
|
}
|
|
|
|
// 检查每日使用上限
|
|
if (_config.dailyLimitMinutes > 0 &&
|
|
todayUsageMinutes >= _config.dailyLimitMinutes) {
|
|
_notifyEvent(EyeCareEvent.dailyLimitReached, {
|
|
'limit_minutes': _config.dailyLimitMinutes,
|
|
'used_minutes': todayUsageMinutes,
|
|
});
|
|
}
|
|
});
|
|
|
|
// 启动距离检测
|
|
if (_config.distanceDetectionEnabled) {
|
|
_startDistanceDetection();
|
|
}
|
|
|
|
// 启动夜间模式检查
|
|
_startNightModeCheck();
|
|
}
|
|
|
|
/// 结束使用(退出学习功能时调用)
|
|
void endSession({String category = 'default'}) {
|
|
_usageTimer?.cancel();
|
|
_usageTimer = null;
|
|
|
|
if (_sessionStartTime != null) {
|
|
final duration = DateTime.now().difference(_sessionStartTime!).inMinutes;
|
|
_categoryUsage[category] = (_categoryUsage[category] ?? 0) + duration;
|
|
}
|
|
|
|
_sessionStartTime = null;
|
|
_continuousUsageSeconds = 0;
|
|
|
|
_distanceTimer?.cancel();
|
|
_distanceTimer = null;
|
|
}
|
|
|
|
/// 用户休息后重置连续使用计时
|
|
void acknowledgeRest() {
|
|
_continuousUsageSeconds = 0;
|
|
}
|
|
|
|
/// 检查当前时间是否在允许使用时段内
|
|
bool _isWithinAllowedTime(DateTime time) {
|
|
final hour = time.hour;
|
|
if (_config.allowedStartHour <= _config.allowedEndHour) {
|
|
return hour >= _config.allowedStartHour && hour < _config.allowedEndHour;
|
|
} else {
|
|
// 跨午夜的情况
|
|
return hour >= _config.allowedStartHour || hour < _config.allowedEndHour;
|
|
}
|
|
}
|
|
|
|
/// 启动监控
|
|
void _startMonitoring() {
|
|
_startNightModeCheck();
|
|
}
|
|
|
|
/// 停止监控
|
|
void _stopMonitoring() {
|
|
_usageTimer?.cancel();
|
|
_distanceTimer?.cancel();
|
|
_nightModeTimer?.cancel();
|
|
}
|
|
|
|
/// 启动距离检测(通过前置摄像头估算用眼距离)
|
|
void _startDistanceDetection() {
|
|
_distanceTimer?.cancel();
|
|
_distanceTimer = Timer.periodic(const Duration(seconds: 10), (_) {
|
|
// 调用前置摄像头进行人脸检测
|
|
// 通过人脸框大小估算距离(人脸越大=距离越近)
|
|
_checkEyeDistance();
|
|
});
|
|
}
|
|
|
|
/// 检查用眼距离(基于前置摄像头人脸检测)
|
|
void _checkEyeDistance() {
|
|
// 实际实现:
|
|
// 1. 使用CameraController获取前置摄像头预览帧
|
|
// 2. 使用MLKit/TFLite进行人脸检测
|
|
// 3. 根据人脸框宽度估算距离: distance = (focal_length * real_face_width) / face_width_in_pixels
|
|
// 4. 本地处理,不上传图像数据(隐私保护)
|
|
|
|
// 模拟距离检测结果
|
|
final estimatedDistanceCm = 35; // 实际从摄像头计算
|
|
|
|
if (estimatedDistanceCm < _config.safeDistanceCm) {
|
|
_notifyEvent(EyeCareEvent.tooCloseWarning, {
|
|
'current_distance': estimatedDistanceCm,
|
|
'safe_distance': _config.safeDistanceCm,
|
|
});
|
|
}
|
|
}
|
|
|
|
/// 启动夜间模式检查
|
|
void _startNightModeCheck() {
|
|
_nightModeTimer?.cancel();
|
|
_nightModeTimer = Timer.periodic(const Duration(minutes: 1), (_) {
|
|
final hour = DateTime.now().hour;
|
|
final shouldBeNightMode = _isNightTimeHour(hour);
|
|
|
|
if (shouldBeNightMode && !_isNightMode) {
|
|
_isNightMode = true;
|
|
_notifyEvent(EyeCareEvent.nightModeOn, {});
|
|
} else if (!shouldBeNightMode && _isNightMode) {
|
|
_isNightMode = false;
|
|
_notifyEvent(EyeCareEvent.nightModeOff, {});
|
|
}
|
|
});
|
|
|
|
// 立即检查一次
|
|
final hour = DateTime.now().hour;
|
|
_isNightMode = _isNightTimeHour(hour);
|
|
}
|
|
|
|
/// 判断是否为夜间时段
|
|
bool _isNightTimeHour(int hour) {
|
|
if (_config.nightModeStartHour <= _config.nightModeEndHour) {
|
|
return hour >= _config.nightModeStartHour && hour < _config.nightModeEndHour;
|
|
} else {
|
|
return hour >= _config.nightModeStartHour || hour < _config.nightModeEndHour;
|
|
}
|
|
}
|
|
|
|
/// 获取今日使用统计
|
|
List<UsageRecord> getTodayUsageRecords() {
|
|
final today = DateTime.now().toString().substring(0, 10);
|
|
return _categoryUsage.entries.map((e) => UsageRecord(
|
|
date: today,
|
|
category: e.key,
|
|
durationMinutes: e.value,
|
|
sessionCount: 1,
|
|
)).toList();
|
|
}
|
|
|
|
/// 通知事件到所有回调
|
|
void _notifyEvent(EyeCareEvent event, Map<String, dynamic> data) {
|
|
for (final callback in _callbacks) {
|
|
try {
|
|
callback(event, data);
|
|
} catch (e) {
|
|
// 忽略回调异常
|
|
}
|
|
}
|
|
}
|
|
|
|
/// 释放资源
|
|
void dispose() {
|
|
_usageTimer?.cancel();
|
|
_distanceTimer?.cancel();
|
|
_nightModeTimer?.cancel();
|
|
_callbacks.clear();
|
|
}
|
|
}
|