Files
AutoTime-Tracker/lib/screens/goal_setting_screen.dart
2025-11-13 15:45:28 +08:00

495 lines
15 KiB
Dart
Raw 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 '../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('保存'),
),
],
);
}
}