first commit
This commit is contained in:
494
lib/screens/goal_setting_screen.dart
Normal file
494
lib/screens/goal_setting_screen.dart
Normal file
@@ -0,0 +1,494 @@
|
||||
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('保存'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user