Files
FocusBuddy/lib/screens/session_detail_screen.dart
2025-11-27 13:37:10 +08:00

582 lines
19 KiB
Dart

import 'package:flutter/material.dart';
import '../l10n/app_localizations.dart';
import '../theme/app_colors.dart';
import '../theme/app_text_styles.dart';
import '../models/focus_session.dart';
import '../models/achievement_config.dart';
import '../services/points_service.dart';
import '../services/storage_service.dart';
import '../services/encouragement_service.dart';
import '../services/di.dart';
/// Session Detail Screen - Shows detailed information about a past focus session
class SessionDetailScreen extends StatelessWidget {
final FocusSession session;
const SessionDetailScreen({super.key, required this.session});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final pointsService = getIt<PointsService>();
final storageService = getIt<StorageService>();
final encouragementService = getIt<EncouragementService>();
// Calculate points for this session
final pointsBreakdown = pointsService.calculateSessionPoints(session);
final pointsEarned = pointsBreakdown['total'] as int;
final basePoints = pointsBreakdown['basePoints'] as int;
final honestyBonus = pointsBreakdown['honestyBonus'] as int;
// Get user progress to show total points
final progress = storageService.getUserProgress();
final encouragement = encouragementService.getRandomMessage();
// Find achievements that might have been unlocked during this session
final sessionAchievements = _findSessionAchievements(
session,
storageService,
);
return Scaffold(
backgroundColor: AppColors.background,
appBar: AppBar(
title: const Text('会话详情'),
backgroundColor: AppColors.background,
),
body: SafeArea(
child: Padding(
padding: const EdgeInsets.all(24.0),
child: SingleChildScrollView(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const SizedBox(height: 20),
// Session Date and Time
_buildSessionHeader(context, l10n),
const SizedBox(height: 32),
// Focused Time Section
Text(l10n.youFocusedFor, style: AppTextStyles.headline),
const SizedBox(height: 8),
Text(
l10n.minutesValue(
session.actualMinutes,
l10n.minutes(session.actualMinutes),
),
style: AppTextStyles.largeNumber,
),
const SizedBox(height: 32),
// Points Earned Section
_buildPointsCard(
context,
l10n,
pointsEarned,
basePoints,
honestyBonus,
),
const SizedBox(height: 16),
// Session Stats Card
Container(
width: double.infinity,
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
color: AppColors.white,
borderRadius: BorderRadius.circular(16),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('会话统计', style: AppTextStyles.headline),
const SizedBox(height: 16),
_buildStatRow(
icon: '⏱️',
label: '计划时长',
value: l10n.minutesValue(
session.durationMinutes,
l10n.minutes(session.durationMinutes),
),
),
const SizedBox(height: 12),
_buildStatRow(
icon: '',
label: '实际专注',
value: l10n.minutesValue(
session.actualMinutes,
l10n.minutes(session.actualMinutes),
),
),
const SizedBox(height: 12),
_buildStatRow(
icon: '🤚',
label: '分心次数',
value: l10n.distractionsCount(
session.distractionCount,
l10n.times(session.distractionCount),
),
),
const SizedBox(height: 12),
_buildStatRow(
icon: '🏁',
label: '状态',
value: session.completed
? l10n.completed
: l10n.stoppedEarly,
),
const SizedBox(height: 20),
Text(
'"$encouragement"',
style: AppTextStyles.encouragementQuote,
),
],
),
),
const SizedBox(height: 16),
// Achievements Unlocked Section
if (sessionAchievements.isNotEmpty)
..._buildAchievementCards(context, l10n, sessionAchievements),
const SizedBox(height: 24),
// Total Points Display
Text(
l10n.totalPoints(progress.totalPoints),
style: const TextStyle(
fontFamily: 'Nunito',
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppColors.primary,
),
),
const SizedBox(height: 40),
],
),
),
),
),
);
}
/// Build session header with date and time
Widget _buildSessionHeader(BuildContext context, AppLocalizations l10n) {
final dateStr = session.startTime.toLocal().toString().split(' ')[0];
final timeStr = session.startTime
.toLocal()
.toString()
.split(' ')[1]
.substring(0, 5);
return Container(
width: double.infinity,
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: AppColors.white,
borderRadius: BorderRadius.circular(16),
),
child: Column(
children: [
Text(dateStr, style: AppTextStyles.headline),
const SizedBox(height: 8),
Text(timeStr, style: AppTextStyles.largeNumber),
],
),
);
}
/// Build points earned card
Widget _buildPointsCard(
BuildContext context,
AppLocalizations l10n,
int pointsEarned,
int basePoints,
int honestyBonus,
) {
return Container(
width: double.infinity,
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: AppColors.white,
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: AppColors.primary.withValues(alpha: 0.3),
width: 2,
),
),
child: Column(
children: [
// Main points display
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
l10n.earnedPoints,
style: TextStyle(
fontFamily: 'Nunito',
fontSize: 18,
color: AppColors.textSecondary,
),
),
const SizedBox(width: 8),
Text(
'+$pointsEarned',
style: const TextStyle(
fontFamily: 'Nunito',
fontSize: 28,
fontWeight: FontWeight.bold,
color: AppColors.primary,
),
),
const Text('', style: TextStyle(fontSize: 24)),
],
),
const SizedBox(height: 16),
Divider(
thickness: 1,
color: AppColors.textSecondary.withValues(alpha: 0.2),
),
const SizedBox(height: 12),
// Points breakdown
_buildPointRow(l10n.basePoints, '+$basePoints', AppColors.success),
if (honestyBonus > 0) ...[
const SizedBox(height: 8),
_buildPointRow(
l10n.honestyBonus,
'+$honestyBonus',
AppColors.success,
subtitle: l10n.distractionsRecorded(
session.distractionCount,
l10n.distractions(session.distractionCount),
),
),
],
],
),
);
}
/// Build a single point row in the breakdown
Widget _buildPointRow(
String label,
String points,
Color color, {
String? subtitle,
}) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 2),
child: Column(
children: [
Row(
children: [
Text(
'├─ ',
style: TextStyle(
color: AppColors.textSecondary.withValues(alpha: 0.4),
fontFamily: 'Nunito',
),
),
Text(
label,
style: const TextStyle(
fontFamily: 'Nunito',
fontSize: 14,
color: AppColors.textSecondary,
),
),
const Spacer(),
Text(
points,
style: TextStyle(
fontFamily: 'Nunito',
fontSize: 16,
fontWeight: FontWeight.w600,
color: color,
),
),
],
),
if (subtitle != null)
Padding(
padding: const EdgeInsets.only(left: 24, top: 4),
child: Row(
children: [
Text(
subtitle,
style: TextStyle(
fontFamily: 'Nunito',
fontSize: 12,
color: AppColors.textSecondary.withValues(alpha: 0.7),
),
),
],
),
),
],
),
);
}
/// Build a single stat row
Widget _buildStatRow({
required String icon,
required String label,
required String value,
}) {
return Row(
children: [
Text(icon, style: const TextStyle(fontSize: 20)),
const SizedBox(width: 12),
Text(label, style: AppTextStyles.bodyText),
const Spacer(),
Text(
value,
style: const TextStyle(
fontFamily: 'Nunito',
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppColors.textPrimary,
),
),
],
);
}
/// Find achievements that might have been unlocked during this session
List<AchievementConfig> _findSessionAchievements(
FocusSession session,
StorageService storageService,
) {
final allAchievements = AchievementConfig.all;
final unlockedAchievements = storageService
.getUserProgress()
.unlockedAchievements;
final sessionAchievements = <AchievementConfig>[];
// Get all sessions to determine the state before this one
final allSessions = storageService.getAllSessions();
final sessionIndex = allSessions.indexOf(session);
// Calculate stats before this session
int sessionsBefore = sessionIndex;
int distractionsBefore = allSessions
.sublist(0, sessionIndex)
.fold(0, (sum, s) => sum + s.distractionCount);
int minutesBefore = allSessions
.sublist(0, sessionIndex)
.fold(0, (sum, s) => sum + s.actualMinutes);
// Check which achievements might have been unlocked by this session
for (final achievement in allAchievements) {
// Skip if not unlocked
if (!unlockedAchievements.containsKey(achievement.id)) {
continue;
}
// Check if this session could have unlocked the achievement
bool unlockedByThisSession = false;
switch (achievement.type) {
case AchievementType.sessionCount:
unlockedByThisSession =
sessionsBefore < achievement.requiredValue &&
(sessionsBefore + 1) >= achievement.requiredValue;
break;
case AchievementType.distractionCount:
unlockedByThisSession =
distractionsBefore < achievement.requiredValue &&
(distractionsBefore + session.distractionCount) >=
achievement.requiredValue;
break;
case AchievementType.totalMinutes:
unlockedByThisSession =
minutesBefore < achievement.requiredValue &&
(minutesBefore + session.actualMinutes) >=
achievement.requiredValue;
break;
case AchievementType.consecutiveDays:
// Consecutive days are not directly related to a single session
// but rather to check-ins, so we'll skip this type
break;
}
if (unlockedByThisSession) {
sessionAchievements.add(achievement);
}
}
return sessionAchievements;
}
/// Build achievement cards for achievements unlocked in this session
List<Widget> _buildAchievementCards(
BuildContext context,
AppLocalizations l10n,
List<AchievementConfig> achievements,
) {
return [
Text('解锁的成就', style: AppTextStyles.headline),
const SizedBox(height: 16),
...achievements.map((achievement) {
return Container(
margin: const EdgeInsets.only(bottom: 16),
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [Color(0xFFFFD700), Color(0xFFFFC107)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.orange.withValues(alpha: 0.4),
blurRadius: 20,
offset: const Offset(0, 8),
),
],
),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(achievement.icon, style: const TextStyle(fontSize: 32)),
const SizedBox(width: 12),
Text(
'成就解锁!',
style: const TextStyle(
fontFamily: 'Nunito',
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
],
),
const SizedBox(height: 12),
Text(
_getLocalizedAchievementName(l10n, achievement.nameKey),
style: const TextStyle(
fontFamily: 'Nunito',
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
const SizedBox(height: 4),
Text(
_getLocalizedAchievementDesc(l10n, achievement.descKey),
style: const TextStyle(
fontFamily: 'Nunito',
fontSize: 14,
color: Colors.white,
),
textAlign: TextAlign.center,
),
if (achievement.bonusPoints > 0)
Padding(
padding: const EdgeInsets.only(top: 8),
child: Text(
'+${achievement.bonusPoints} 积分',
style: const TextStyle(
fontFamily: 'Nunito',
fontSize: 16,
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
),
],
),
);
}),
];
}
/// Get localized achievement name by key
String _getLocalizedAchievementName(AppLocalizations l10n, String key) {
switch (key) {
case 'achievement_first_session_name':
return l10n.achievement_first_session_name;
case 'achievement_sessions_10_name':
return l10n.achievement_sessions_10_name;
case 'achievement_sessions_50_name':
return l10n.achievement_sessions_50_name;
case 'achievement_sessions_100_name':
return l10n.achievement_sessions_100_name;
case 'achievement_honest_bronze_name':
return l10n.achievement_honest_bronze_name;
case 'achievement_honest_silver_name':
return l10n.achievement_honest_silver_name;
case 'achievement_honest_gold_name':
return l10n.achievement_honest_gold_name;
case 'achievement_marathon_name':
return l10n.achievement_marathon_name;
case 'achievement_century_name':
return l10n.achievement_century_name;
case 'achievement_master_name':
return l10n.achievement_master_name;
case 'achievement_persistence_star_name':
return l10n.achievement_persistence_star_name;
case 'achievement_monthly_habit_name':
return l10n.achievement_monthly_habit_name;
case 'achievement_centurion_name':
return l10n.achievement_centurion_name;
case 'achievement_year_warrior_name':
return l10n.achievement_year_warrior_name;
default:
return key;
}
}
/// Get localized achievement description by key
String _getLocalizedAchievementDesc(AppLocalizations l10n, String key) {
switch (key) {
case 'achievement_first_session_desc':
return l10n.achievement_first_session_desc;
case 'achievement_sessions_10_desc':
return l10n.achievement_sessions_10_desc;
case 'achievement_sessions_50_desc':
return l10n.achievement_sessions_50_desc;
case 'achievement_sessions_100_desc':
return l10n.achievement_sessions_100_desc;
case 'achievement_honest_bronze_desc':
return l10n.achievement_honest_bronze_desc;
case 'achievement_honest_silver_desc':
return l10n.achievement_honest_silver_desc;
case 'achievement_honest_gold_desc':
return l10n.achievement_honest_gold_desc;
case 'achievement_marathon_desc':
return l10n.achievement_marathon_desc;
case 'achievement_century_desc':
return l10n.achievement_century_desc;
case 'achievement_master_desc':
return l10n.achievement_master_desc;
case 'achievement_persistence_star_desc':
return l10n.achievement_persistence_star_desc;
case 'achievement_monthly_habit_desc':
return l10n.achievement_monthly_habit_desc;
case 'achievement_centurion_desc':
return l10n.achievement_centurion_desc;
case 'achievement_year_warrior_desc':
return l10n.achievement_year_warrior_desc;
default:
return key;
}
}
}