first commit

This commit is contained in:
ytc1012
2025-11-13 15:45:28 +08:00
commit 6b321890c0
54 changed files with 8412 additions and 0 deletions

View 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('保存'),
),
],
);
}
}