Files
eryao/apps/lib/features/divination/presentation/screens/divination_screen.dart
T

514 lines
15 KiB
Dart

import 'package:flutter/material.dart';
import '../../../../app/di/injection.dart';
import '../../../../core/auth/session_store.dart';
import '../../../../data/network/api_client.dart';
import '../../../../l10n/app_localizations.dart';
import '../../../../shared/theme/design_tokens.dart';
import '../../../../shared/widgets/divination/divination_shared_widgets.dart';
import '../../../../shared/widgets/toast/toast.dart';
import '../../../../shared/widgets/toast/toast_type.dart';
import '../../data/apis/divination_api.dart';
import '../../data/models/divination_params.dart';
import '../../data/services/divination_run_service.dart';
import 'auto_divination_screen.dart';
import 'manual_divination_screen.dart';
class DivinationScreen extends StatefulWidget {
const DivinationScreen({
super.key,
required this.sessionStore,
required this.userId,
this.runServiceOverride,
});
final SessionStore sessionStore;
final String userId;
final DivinationRunService? runServiceOverride;
@override
State<DivinationScreen> createState() => _DivinationScreenState();
}
class _DivinationScreenState extends State<DivinationScreen> {
late DivinationParams _params;
final TextEditingController _questionController = TextEditingController();
late final DivinationRunService _runService;
@override
void initState() {
super.initState();
final apiClient = ApiClient(
baseUrl: appDependencies.backendUrl,
tokenProvider: widget.sessionStore.getToken,
);
_runService =
widget.runServiceOverride ??
DivinationRunService(api: DivinationApi(apiClient: apiClient));
_params = DivinationParams(
method: DivinationMethod.manual,
questionType: QuestionType.career,
question: '',
divinationTime: DateTime.now(),
coinBalance: 0,
userId: widget.userId,
);
_questionController.addListener(_syncQuestion);
}
@override
void dispose() {
_questionController
..removeListener(_syncQuestion)
..dispose();
super.dispose();
}
void _syncQuestion() {
_params = _params.copyWith(question: _questionController.text.trim());
}
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
final l10n = AppLocalizations.of(context)!;
return Scaffold(
backgroundColor: colors.surface,
appBar: AppBar(
backgroundColor: colors.surface,
surfaceTintColor: colors.surface,
title: Text(l10n.divinationScreenTitle),
centerTitle: true,
),
body: _buildBody(context, l10n),
);
}
Widget _buildBody(BuildContext context, AppLocalizations l10n) {
return GestureDetector(
onTap: () => FocusScope.of(context).unfocus(),
child: SingleChildScrollView(
padding: const EdgeInsets.fromLTRB(
AppSpacing.xl,
AppSpacing.lg,
AppSpacing.xl,
AppSpacing.xl,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_GuideEntryCard(onTap: () => _showGuide(context, l10n)),
const SizedBox(height: AppSpacing.lg),
_MethodSection(
selected: _params.method,
onChanged: (method) {
setState(() {
_params = _params.copyWith(method: method);
});
},
l10n: l10n,
),
const SizedBox(height: AppSpacing.xl),
Text(
l10n.divinationQuestionTypePrompt,
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: AppSpacing.sm),
_QuestionTypeSelector(
selected: _params.questionType,
onChanged: (type) {
setState(() {
_params = _params.copyWith(questionType: type);
});
},
l10n: l10n,
),
const SizedBox(height: AppSpacing.lg),
Text(
l10n.divinationQuestionInputPrompt,
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: AppSpacing.sm),
_QuestionTextField(controller: _questionController, l10n: l10n),
const SizedBox(height: AppSpacing.xl),
_StartButton(onPressed: _onStart, l10n: l10n),
],
),
),
);
}
void _onStart() {
final l10n = AppLocalizations.of(context)!;
if (_params.question.isEmpty) {
Toast.show(
context,
l10n.toastPleaseInputQuestion,
type: ToastType.warning,
);
return;
}
if (_params.method == DivinationMethod.manual) {
final nextParams = _params.copyWith(divinationTime: DateTime.now());
Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (_) => ManualDivinationScreen(
params: nextParams,
runService: _runService,
),
),
);
return;
}
final nextParams = _params.copyWith(divinationTime: DateTime.now());
Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (_) =>
AutoDivinationScreen(params: nextParams, runService: _runService),
),
);
}
}
class _GuideEntryCard extends StatelessWidget {
const _GuideEntryCard({required this.onTap});
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return DivinationInstructionCard(
text: l10n.divinationRecommendManual,
onTap: onTap,
);
}
}
class _MethodSection extends StatelessWidget {
const _MethodSection({
required this.selected,
required this.onChanged,
required this.l10n,
});
final DivinationMethod selected;
final ValueChanged<DivinationMethod> onChanged;
final AppLocalizations l10n;
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text(
l10n.divinationSelectMethod,
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(width: AppSpacing.sm),
IconButton(
onPressed: () => _showMethodTip(context, l10n),
icon: Icon(
Icons.help_outline,
color: Theme.of(context).colorScheme.primary,
),
),
],
),
const SizedBox(height: AppSpacing.sm),
_MethodSegment(selected: selected, onChanged: onChanged, l10n: l10n),
],
);
}
}
class _MethodSegment extends StatelessWidget {
const _MethodSegment({
required this.selected,
required this.onChanged,
required this.l10n,
});
final DivinationMethod selected;
final ValueChanged<DivinationMethod> onChanged;
final AppLocalizations l10n;
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
return SizedBox(
height: 40,
child: Row(
children: [
_SegmentButton(
text: l10n.divinationManualMethod,
selected: selected == DivinationMethod.manual,
onTap: () => onChanged(DivinationMethod.manual),
color: colors.primary,
isLeft: true,
),
_SegmentButton(
text: l10n.divinationAutoMethod,
selected: selected == DivinationMethod.auto,
onTap: () => onChanged(DivinationMethod.auto),
color: colors.primary,
isRight: true,
),
],
),
);
}
}
class _SegmentButton extends StatelessWidget {
const _SegmentButton({
required this.text,
required this.selected,
required this.onTap,
required this.color,
this.isLeft = false,
this.isRight = false,
});
final String text;
final bool selected;
final VoidCallback onTap;
final Color color;
final bool isLeft;
final bool isRight;
@override
Widget build(BuildContext context) {
return Expanded(
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.horizontal(
left: isLeft ? Radius.circular(AppRadius.sm) : Radius.zero,
right: isRight ? Radius.circular(AppRadius.sm) : Radius.zero,
),
child: Container(
decoration: BoxDecoration(
color: selected ? color : color.withValues(alpha: 0),
borderRadius: BorderRadius.horizontal(
left: isLeft ? Radius.circular(AppRadius.sm) : Radius.zero,
right: isRight ? Radius.circular(AppRadius.sm) : Radius.zero,
),
border: Border.all(color: color),
),
alignment: Alignment.center,
child: Text(
text,
style: Theme.of(context).textTheme.titleMedium?.copyWith(
color: selected ? Theme.of(context).colorScheme.onPrimary : color,
fontWeight: FontWeight.w500,
),
),
),
),
);
}
}
class _QuestionTextField extends StatelessWidget {
const _QuestionTextField({required this.controller, required this.l10n});
final TextEditingController controller;
final AppLocalizations l10n;
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
return TextField(
controller: controller,
maxLines: 4,
decoration: InputDecoration(
hintText: l10n.divinationQuestionInputHint,
filled: true,
fillColor: colors.surface,
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(AppRadius.sm),
borderSide: BorderSide(color: colors.outline),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(AppRadius.sm),
borderSide: BorderSide(color: colors.primary),
),
),
);
}
}
class _StartButton extends StatelessWidget {
const _StartButton({required this.onPressed, required this.l10n});
final VoidCallback onPressed;
final AppLocalizations l10n;
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
return SizedBox(
width: double.infinity,
height: 50,
child: FilledButton(
style: FilledButton.styleFrom(
backgroundColor: colors.primary,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppRadius.sm),
),
),
onPressed: onPressed,
child: Text(l10n.divinationStartButton),
),
);
}
}
Future<void> _showMethodTip(BuildContext context, AppLocalizations l10n) {
return showDialog<void>(
context: context,
builder: (context) {
return AlertDialog(
title: Text(l10n.divinationMethodTipTitle),
content: Text(
'${l10n.divinationMethodTipAuto}\n\n${l10n.divinationMethodTipManual}\n\n${l10n.divinationMethodTipRecommend}',
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text(l10n.divinationIAcknowledge),
),
],
);
},
);
}
Future<void> _showGuide(BuildContext context, AppLocalizations l10n) {
return showDialog<void>(
context: context,
builder: (context) {
return DivinationGuideDialog(
title: l10n.divinationManualGuideTitle,
guideImages: const [
'assets/images/qigua/lc1.jpg',
'assets/images/qigua/lc2.jpg',
'assets/images/qigua/lc3.jpg',
'assets/images/qigua/lc4.jpg',
'assets/images/qigua/lc5.jpg',
],
instructionText: l10n.divinationManualGuideInstruction,
);
},
);
}
class _QuestionTypeSelector extends StatelessWidget {
const _QuestionTypeSelector({
required this.selected,
required this.onChanged,
required this.l10n,
});
final QuestionType selected;
final ValueChanged<QuestionType> onChanged;
final AppLocalizations l10n;
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
final types = <(QuestionType, String, IconData)>[
(QuestionType.career, l10n.questionTypeCareer, Icons.work),
(QuestionType.love, l10n.questionTypeLove, Icons.favorite),
(QuestionType.wealth, l10n.questionTypeWealth, Icons.attach_money),
(QuestionType.fortune, l10n.questionTypeFortune, Icons.trending_up),
(QuestionType.dream, l10n.questionTypeDream, Icons.bedtime),
(QuestionType.health, l10n.questionTypeHealth, Icons.health_and_safety),
(QuestionType.study, l10n.questionTypeStudy, Icons.school),
(QuestionType.search, l10n.questionTypeSearch, Icons.search),
(QuestionType.other, l10n.questionTypeOther, Icons.help),
];
return SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: types.map((item) {
final isSelected = selected == item.$1;
return Padding(
padding: const EdgeInsets.only(right: AppSpacing.sm),
child: _TypeChip(
label: item.$2,
icon: item.$3,
isSelected: isSelected,
onTap: () => onChanged(item.$1),
selectedColor: colors.primary,
),
);
}).toList(),
),
);
}
}
class _TypeChip extends StatelessWidget {
const _TypeChip({
required this.label,
required this.icon,
required this.isSelected,
required this.onTap,
required this.selectedColor,
});
final String label;
final IconData icon;
final bool isSelected;
final VoidCallback onTap;
final Color selectedColor;
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
return InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(AppRadius.sm),
child: Container(
width: 92,
height: 45,
decoration: BoxDecoration(
color: isSelected
? selectedColor.withValues(alpha: 0.1)
: colors.surface,
borderRadius: BorderRadius.circular(AppRadius.sm),
border: Border.all(
color: isSelected ? selectedColor : colors.outline,
width: isSelected ? 2 : 1,
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
icon,
size: 20,
color: isSelected ? selectedColor : colors.onSurface,
),
const SizedBox(width: AppSpacing.xs),
Text(
label,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w700,
color: isSelected ? selectedColor : colors.onSurface,
),
),
],
),
),
);
}
}