495 lines
15 KiB
Dart
495 lines
15 KiB
Dart
import 'package:flutter/material.dart';
|
||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||
import '../theme/app_theme.dart';
|
||
import '../models/time_goal.dart';
|
||
import '../database/time_goal_dao.dart';
|
||
|
||
class GoalSettingScreen extends ConsumerStatefulWidget {
|
||
const GoalSettingScreen({super.key});
|
||
|
||
@override
|
||
ConsumerState<GoalSettingScreen> createState() => _GoalSettingScreenState();
|
||
}
|
||
|
||
class _GoalSettingScreenState extends ConsumerState<GoalSettingScreen> {
|
||
final TimeGoalDao _goalDao = TimeGoalDao();
|
||
|
||
TimeGoal? _dailyTotalGoal;
|
||
final Map<String, TimeGoal?> _categoryGoals = {};
|
||
|
||
@override
|
||
void initState() {
|
||
super.initState();
|
||
_loadGoals();
|
||
}
|
||
|
||
Future<void> _loadGoals() async {
|
||
// 获取所有目标(包括非激活的),以便正确显示开关状态
|
||
final allGoals = await _goalDao.getAllGoals();
|
||
|
||
setState(() {
|
||
// 查找每日总时长目标
|
||
final dailyTotal = allGoals.firstWhere(
|
||
(g) => g.goalType == 'daily_total',
|
||
orElse: () => TimeGoal(
|
||
goalType: 'daily_total',
|
||
targetTime: 28800, // 默认 8 小时
|
||
isActive: _dailyTotalGoal?.isActive ?? true, // 保持当前状态
|
||
createdAt: _dailyTotalGoal?.createdAt ?? DateTime.now(),
|
||
updatedAt: DateTime.now(),
|
||
),
|
||
);
|
||
|
||
// 如果找到的目标有 ID,使用它;否则保持当前 ID(如果有)
|
||
_dailyTotalGoal = TimeGoal(
|
||
id: dailyTotal.id ?? _dailyTotalGoal?.id,
|
||
goalType: dailyTotal.goalType,
|
||
targetTime: dailyTotal.targetTime,
|
||
isActive: dailyTotal.isActive,
|
||
createdAt: dailyTotal.createdAt,
|
||
updatedAt: dailyTotal.updatedAt,
|
||
);
|
||
|
||
final categories = ['work', 'study', 'entertainment', 'social', 'tool'];
|
||
for (final category in categories) {
|
||
_categoryGoals[category] = allGoals.firstWhere(
|
||
(g) => g.goalType == 'daily_category' && g.category == category,
|
||
orElse: () => TimeGoal(
|
||
goalType: 'daily_category',
|
||
category: category,
|
||
targetTime: 0,
|
||
isActive: false,
|
||
createdAt: DateTime.now(),
|
||
updatedAt: DateTime.now(),
|
||
),
|
||
);
|
||
}
|
||
});
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final theme = Theme.of(context);
|
||
|
||
return Scaffold(
|
||
appBar: AppBar(
|
||
title: const Text('时间目标'),
|
||
),
|
||
body: SingleChildScrollView(
|
||
padding: const EdgeInsets.all(16),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
// 每日总时长目标
|
||
_buildDailyTotalGoalCard(theme),
|
||
const SizedBox(height: 16),
|
||
|
||
// 分类时间限制
|
||
_buildCategoryGoalsCard(theme),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildDailyTotalGoalCard(ThemeData theme) {
|
||
return Card(
|
||
child: Padding(
|
||
padding: const EdgeInsets.all(16),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Row(
|
||
children: [
|
||
Icon(Icons.timer, color: AppTheme.primaryColor),
|
||
const SizedBox(width: 8),
|
||
Text(
|
||
'每日总时长目标',
|
||
style: theme.textTheme.titleMedium?.copyWith(
|
||
fontWeight: FontWeight.w600,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
const SizedBox(height: 16),
|
||
if (_dailyTotalGoal != null) ...[
|
||
Row(
|
||
children: [
|
||
Expanded(
|
||
child: Text(
|
||
_dailyTotalGoal!.formattedTargetTime,
|
||
style: theme.textTheme.headlineSmall?.copyWith(
|
||
fontWeight: FontWeight.bold,
|
||
color: AppTheme.primaryColor,
|
||
),
|
||
),
|
||
),
|
||
Switch(
|
||
value: _dailyTotalGoal!.isActive,
|
||
onChanged: (value) async {
|
||
// 先更新本地状态,立即显示变化
|
||
setState(() {
|
||
_dailyTotalGoal = TimeGoal(
|
||
id: _dailyTotalGoal!.id,
|
||
goalType: _dailyTotalGoal!.goalType,
|
||
targetTime: _dailyTotalGoal!.targetTime,
|
||
isActive: value,
|
||
createdAt: _dailyTotalGoal!.createdAt,
|
||
updatedAt: DateTime.now(),
|
||
);
|
||
});
|
||
|
||
// 然后保存到数据库
|
||
await _goalDao.upsertTimeGoal(_dailyTotalGoal!);
|
||
// 重新加载以确保数据同步
|
||
await _loadGoals();
|
||
},
|
||
),
|
||
],
|
||
),
|
||
const SizedBox(height: 16),
|
||
_buildTimePicker(
|
||
theme,
|
||
'设置目标时长',
|
||
_dailyTotalGoal!.targetTime,
|
||
(hours, minutes) async {
|
||
final targetTime = hours * 3600 + minutes * 60;
|
||
final updatedGoal = TimeGoal(
|
||
id: _dailyTotalGoal!.id,
|
||
goalType: 'daily_total',
|
||
targetTime: targetTime,
|
||
isActive: _dailyTotalGoal!.isActive,
|
||
createdAt: _dailyTotalGoal!.createdAt,
|
||
updatedAt: DateTime.now(),
|
||
);
|
||
await _goalDao.upsertTimeGoal(updatedGoal);
|
||
await _loadGoals();
|
||
},
|
||
),
|
||
],
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildCategoryGoalsCard(ThemeData theme) {
|
||
return Card(
|
||
child: Padding(
|
||
padding: const EdgeInsets.all(16),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Row(
|
||
children: [
|
||
Icon(Icons.category, color: AppTheme.primaryColor),
|
||
const SizedBox(width: 8),
|
||
Text(
|
||
'分类时间限制',
|
||
style: theme.textTheme.titleMedium?.copyWith(
|
||
fontWeight: FontWeight.w600,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
const SizedBox(height: 16),
|
||
...['work', 'study', 'entertainment', 'social', 'tool'].map((category) {
|
||
final goal = _categoryGoals[category];
|
||
return _buildCategoryGoalItem(theme, category, goal);
|
||
}),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildCategoryGoalItem(
|
||
ThemeData theme,
|
||
String category,
|
||
TimeGoal? goal,
|
||
) {
|
||
final isActive = goal?.isActive ?? false;
|
||
final targetTime = goal?.targetTime ?? 0;
|
||
|
||
return Padding(
|
||
padding: const EdgeInsets.only(bottom: 12),
|
||
child: Row(
|
||
children: [
|
||
Container(
|
||
width: 12,
|
||
height: 12,
|
||
decoration: BoxDecoration(
|
||
color: AppTheme.getCategoryColor(category),
|
||
shape: BoxShape.circle,
|
||
),
|
||
),
|
||
const SizedBox(width: 12),
|
||
Expanded(
|
||
child: Text(
|
||
AppTheme.getCategoryName(category),
|
||
style: theme.textTheme.bodyLarge,
|
||
),
|
||
),
|
||
Text(
|
||
targetTime > 0 ? _formatTime(targetTime) : '未设置',
|
||
style: theme.textTheme.bodyMedium?.copyWith(
|
||
color: isActive ? AppTheme.primaryColor : theme.colorScheme.onSurface.withOpacity(0.5),
|
||
fontWeight: isActive ? FontWeight.w600 : FontWeight.normal,
|
||
),
|
||
),
|
||
const SizedBox(width: 12),
|
||
Switch(
|
||
value: isActive,
|
||
onChanged: (value) async {
|
||
if (value && targetTime == 0) {
|
||
// 如果启用但未设置时间,先设置默认值
|
||
final defaultGoal = TimeGoal(
|
||
id: goal?.id,
|
||
goalType: 'daily_category',
|
||
category: category,
|
||
targetTime: 7200, // 默认 2 小时
|
||
isActive: true,
|
||
createdAt: goal?.createdAt ?? DateTime.now(),
|
||
updatedAt: DateTime.now(),
|
||
);
|
||
await _goalDao.upsertTimeGoal(defaultGoal);
|
||
} else {
|
||
final updatedGoal = TimeGoal(
|
||
id: goal?.id,
|
||
goalType: 'daily_category',
|
||
category: category,
|
||
targetTime: targetTime,
|
||
isActive: value,
|
||
createdAt: goal?.createdAt ?? DateTime.now(),
|
||
updatedAt: DateTime.now(),
|
||
);
|
||
await _goalDao.upsertTimeGoal(updatedGoal);
|
||
}
|
||
await _loadGoals();
|
||
},
|
||
),
|
||
IconButton(
|
||
icon: const Icon(Icons.edit),
|
||
onPressed: () {
|
||
_showCategoryGoalDialog(theme, category, goal);
|
||
},
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
void _showCategoryGoalDialog(ThemeData theme, String category, TimeGoal? goal) {
|
||
final currentTime = goal?.targetTime ?? 0;
|
||
final hours = currentTime ~/ 3600;
|
||
final minutes = (currentTime % 3600) ~/ 60;
|
||
|
||
showDialog(
|
||
context: context,
|
||
builder: (context) => _TimePickerDialog(
|
||
title: '设置 ${AppTheme.getCategoryName(category)} 时间限制',
|
||
initialHours: hours,
|
||
initialMinutes: minutes,
|
||
onSave: (hours, minutes) async {
|
||
final targetTime = hours * 3600 + minutes * 60;
|
||
final updatedGoal = TimeGoal(
|
||
id: goal?.id,
|
||
goalType: 'daily_category',
|
||
category: category,
|
||
targetTime: targetTime,
|
||
isActive: true,
|
||
createdAt: goal?.createdAt ?? DateTime.now(),
|
||
updatedAt: DateTime.now(),
|
||
);
|
||
await _goalDao.upsertTimeGoal(updatedGoal);
|
||
await _loadGoals();
|
||
},
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildTimePicker(
|
||
ThemeData theme,
|
||
String title,
|
||
int currentTime,
|
||
Future<void> Function(int hours, int minutes) onSave,
|
||
) {
|
||
final hours = currentTime ~/ 3600;
|
||
final minutes = (currentTime % 3600) ~/ 60;
|
||
|
||
return ElevatedButton.icon(
|
||
onPressed: () {
|
||
showDialog(
|
||
context: context,
|
||
builder: (context) => _TimePickerDialog(
|
||
title: title,
|
||
initialHours: hours,
|
||
initialMinutes: minutes,
|
||
onSave: onSave,
|
||
),
|
||
);
|
||
},
|
||
icon: const Icon(Icons.access_time),
|
||
label: Text('${hours}小时 ${minutes}分钟'),
|
||
style: ElevatedButton.styleFrom(
|
||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||
),
|
||
);
|
||
}
|
||
|
||
String _formatTime(int seconds) {
|
||
final hours = seconds ~/ 3600;
|
||
final minutes = (seconds % 3600) ~/ 60;
|
||
if (hours > 0) {
|
||
return '${hours}h ${minutes}m';
|
||
}
|
||
return '${minutes}m';
|
||
}
|
||
}
|
||
|
||
class _TimePickerDialog extends StatefulWidget {
|
||
final String title;
|
||
final int initialHours;
|
||
final int initialMinutes;
|
||
final Future<void> Function(int hours, int minutes) onSave;
|
||
|
||
const _TimePickerDialog({
|
||
required this.title,
|
||
required this.initialHours,
|
||
required this.initialMinutes,
|
||
required this.onSave,
|
||
});
|
||
|
||
@override
|
||
State<_TimePickerDialog> createState() => _TimePickerDialogState();
|
||
}
|
||
|
||
class _TimePickerDialogState extends State<_TimePickerDialog> {
|
||
late int _hours;
|
||
late int _minutes;
|
||
|
||
@override
|
||
void initState() {
|
||
super.initState();
|
||
_hours = widget.initialHours;
|
||
_minutes = widget.initialMinutes;
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final theme = Theme.of(context);
|
||
|
||
return AlertDialog(
|
||
title: Text(widget.title),
|
||
content: Column(
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
Row(
|
||
mainAxisAlignment: MainAxisAlignment.center,
|
||
children: [
|
||
// 小时选择
|
||
Column(
|
||
children: [
|
||
Text('小时', style: theme.textTheme.bodySmall),
|
||
const SizedBox(height: 8),
|
||
Row(
|
||
children: [
|
||
IconButton(
|
||
icon: const Icon(Icons.remove_circle_outline),
|
||
onPressed: () {
|
||
setState(() {
|
||
if (_hours > 0) _hours--;
|
||
});
|
||
},
|
||
),
|
||
SizedBox(
|
||
width: 60,
|
||
child: Text(
|
||
'$_hours',
|
||
textAlign: TextAlign.center,
|
||
style: theme.textTheme.headlineMedium,
|
||
),
|
||
),
|
||
IconButton(
|
||
icon: const Icon(Icons.add_circle_outline),
|
||
onPressed: () {
|
||
setState(() {
|
||
if (_hours < 24) _hours++;
|
||
});
|
||
},
|
||
),
|
||
],
|
||
),
|
||
],
|
||
),
|
||
const SizedBox(width: 24),
|
||
// 分钟选择
|
||
Column(
|
||
children: [
|
||
Text('分钟', style: theme.textTheme.bodySmall),
|
||
const SizedBox(height: 8),
|
||
Row(
|
||
children: [
|
||
IconButton(
|
||
icon: const Icon(Icons.remove_circle_outline),
|
||
onPressed: () {
|
||
setState(() {
|
||
if (_minutes > 0) {
|
||
_minutes -= 15;
|
||
if (_minutes < 0) _minutes = 0;
|
||
}
|
||
});
|
||
},
|
||
),
|
||
SizedBox(
|
||
width: 60,
|
||
child: Text(
|
||
'$_minutes',
|
||
textAlign: TextAlign.center,
|
||
style: theme.textTheme.headlineMedium,
|
||
),
|
||
),
|
||
IconButton(
|
||
icon: const Icon(Icons.add_circle_outline),
|
||
onPressed: () {
|
||
setState(() {
|
||
_minutes += 15;
|
||
if (_minutes >= 60) {
|
||
_hours++;
|
||
_minutes = 0;
|
||
}
|
||
if (_hours >= 24) {
|
||
_hours = 23;
|
||
_minutes = 59;
|
||
}
|
||
});
|
||
},
|
||
),
|
||
],
|
||
),
|
||
],
|
||
),
|
||
],
|
||
),
|
||
],
|
||
),
|
||
actions: [
|
||
TextButton(
|
||
onPressed: () => Navigator.of(context).pop(),
|
||
child: const Text('取消'),
|
||
),
|
||
ElevatedButton(
|
||
onPressed: () async {
|
||
await widget.onSave(_hours, _minutes);
|
||
if (mounted) {
|
||
Navigator.of(context).pop();
|
||
}
|
||
},
|
||
child: const Text('保存'),
|
||
),
|
||
],
|
||
);
|
||
}
|
||
}
|
||
|