feat: integrate invite API and improve notification handling

- Add invite code display and binding functionality via API
- Fix notification unread count sync on auth state change
- Improve notification mark read with server state validation
- Add auth state listener to trigger notification refresh
- Add YaoCoinConverter for coin-to-yao type conversion
- Remove YaoLegend from divination screens (UI cleanup)
- Abbreviate relation labels in yao detail view
- Add re-register notice to account delete screen
- Update 'coins' terminology to 'points' in localization
- Fix backend points consumption to only run in CHAT mode
- Add HttpxAuthNoiseFilter to suppress auth endpoint logging
- Fix notification static_schema import path
- Add test coverage for notification bloc error handling
- Update AGENTS.md page header rules and image handling
- Delete deprecated run-dev.sh script
This commit is contained in:
qzl
2026-04-13 14:52:22 +08:00
parent da947f9f08
commit 1e22f27de2
52 changed files with 1419 additions and 307 deletions
@@ -208,6 +208,24 @@ class _DeleteConfirmDialogState extends State<_DeleteConfirmDialog> {
height: 1.45,
),
),
const SizedBox(height: AppSpacing.sm),
Container(
width: double.infinity,
padding: const EdgeInsets.all(AppSpacing.md),
decoration: BoxDecoration(
color: colors.errorContainer,
borderRadius: BorderRadius.circular(AppRadius.md),
border: Border.all(color: colors.error.withValues(alpha: 0.35)),
),
child: Text(
l10n.settingsDeleteAccountReRegisterNotice,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: colors.onErrorContainer,
fontWeight: FontWeight.w700,
height: 1.35,
),
),
),
const SizedBox(height: AppSpacing.md),
Text(
_secondsLeft > 0
@@ -1,44 +1,125 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../../../../core/logging/logger.dart';
import '../../../../l10n/app_localizations.dart';
import '../../../../shared/theme/app_color_palette.dart';
import '../../../../shared/theme/design_tokens.dart';
import '../../../../shared/widgets/toast/toast.dart';
import '../../../../shared/widgets/toast/toast_type.dart';
import '../../data/repositories/invite_repository.dart';
class InviteScreen extends StatefulWidget {
const InviteScreen({super.key});
const InviteScreen({super.key, required this.inviteRepository});
final InviteRepository inviteRepository;
@override
State<InviteScreen> createState() => _InviteScreenState();
}
class _InviteScreenState extends State<InviteScreen> {
final Logger _logger = getLogger('features.settings.invite_screen');
final _bindCodeController = TextEditingController();
final _formKey = GlobalKey<FormState>();
bool _isBinding = false;
bool _isGenerating = false;
bool _isLoading = true;
bool _hasError = false;
// Mock data - will be replaced with API calls
final String _myInviteCode = 'ABC123';
final int _invitedCount = 3;
String? _myInviteCode;
int _invitedCount = 0;
final bool _hasInviter = false;
@override
void initState() {
super.initState();
_loadInviteCode();
}
Future<void> _loadInviteCode() async {
setState(() {
_isLoading = true;
_hasError = false;
});
try {
final result = await widget.inviteRepository.getMyInviteCode();
if (!mounted) return;
setState(() {
_myInviteCode = result.code;
_invitedCount = result.usedCount;
_isLoading = false;
});
} catch (error, stackTrace) {
_logger.error(
message: 'Failed to load invite code',
error: error,
stackTrace: stackTrace,
);
if (!mounted) return;
setState(() {
_hasError = true;
_isLoading = false;
});
}
}
@override
void dispose() {
_bindCodeController.dispose();
super.dispose();
}
bool get _hasMyInviteCode => _myInviteCode.isNotEmpty;
bool get _hasMyInviteCode =>
_myInviteCode != null && _myInviteCode!.isNotEmpty;
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final colors = Theme.of(context).colorScheme;
if (_isLoading) {
return Scaffold(
backgroundColor: colors.surfaceContainerLow,
appBar: AppBar(
title: Text(l10n.settingsInviteTitle),
centerTitle: true,
backgroundColor: colors.surfaceContainerLow,
surfaceTintColor: colors.surfaceContainerLow,
),
body: const Center(child: CircularProgressIndicator()),
);
}
if (_hasError) {
return Scaffold(
backgroundColor: colors.surfaceContainerLow,
appBar: AppBar(
title: Text(l10n.settingsInviteTitle),
centerTitle: true,
backgroundColor: colors.surfaceContainerLow,
surfaceTintColor: colors.surfaceContainerLow,
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
l10n.settingsInviteEmptyTitle,
style: Theme.of(context).textTheme.bodyLarge,
),
const SizedBox(height: AppSpacing.md),
FilledButton(
onPressed: _loadInviteCode,
child: const Text('Retry'),
),
],
),
),
);
}
return Scaffold(
backgroundColor: colors.surfaceContainerLow,
appBar: AppBar(
@@ -51,7 +132,10 @@ class _InviteScreenState extends State<InviteScreen> {
padding: const EdgeInsets.all(AppSpacing.lg),
children: [
if (_hasMyInviteCode) ...[
_InviteCodeCard(inviteCode: _myInviteCode, onCopy: _copyInviteCode),
_InviteCodeCard(
inviteCode: _myInviteCode!,
onCopy: _copyInviteCode,
),
const SizedBox(height: AppSpacing.lg),
_InviteStatsCard(count: _invitedCount),
const SizedBox(height: AppSpacing.xl),
@@ -79,7 +163,7 @@ class _InviteScreenState extends State<InviteScreen> {
void _copyInviteCode() {
final l10n = AppLocalizations.of(context)!;
Clipboard.setData(ClipboardData(text: _myInviteCode));
Clipboard.setData(ClipboardData(text: _myInviteCode!));
Toast.show(
context,
l10n.settingsInviteCopySuccess,
@@ -5,6 +5,7 @@ import '../../../../shared/theme/design_tokens.dart';
import '../../../../shared/widgets/app_modal_dialog.dart';
import '../../../../shared/widgets/gua_icon.dart';
import '../../data/models/profile_settings.dart';
import '../../data/repositories/invite_repository.dart';
import 'account_delete_screen.dart';
import '../widgets/settings_section_widgets.dart';
import 'coin_center_screen.dart';
@@ -19,6 +20,7 @@ class SettingsScreen extends StatefulWidget {
required this.account,
required this.settings,
required this.coinBalance,
required this.inviteRepository,
required this.onInterfaceLanguageChanged,
required this.onSettingsChanged,
required this.onUploadAvatar,
@@ -30,6 +32,7 @@ class SettingsScreen extends StatefulWidget {
final String account;
final ProfileSettingsV1 settings;
final int coinBalance;
final InviteRepository inviteRepository;
final Future<void> Function(String languageTag) onInterfaceLanguageChanged;
final Future<void> Function(ProfileSettingsV1 settings) onSettingsChanged;
final Future<ProfileSettingsV1> Function(String filePath) onUploadAvatar;
@@ -179,9 +182,11 @@ class _SettingsScreenState extends State<SettingsScreen> {
}
Future<void> _openInvite() async {
await Navigator.of(
context,
).push<void>(MaterialPageRoute<void>(builder: (_) => const InviteScreen()));
await Navigator.of(context).push<void>(
MaterialPageRoute<void>(
builder: (_) => InviteScreen(inviteRepository: widget.inviteRepository),
),
);
}
Future<void> _openProfileEdit() async {