Files
ReelShort/lib/pages/account/account_profile_page.dart
T

314 lines
9.2 KiB
Dart

// Author: fruit.com.cn
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../../core/data/models/account_profile.dart';
import '../../core/data/repositories/account_repository.dart';
import '../../core/data/services/auth_controller.dart';
import '../../core/formatting/error_text.dart';
import '../../core/formatting/localized_date_time.dart';
import '../../core/theme/app_colors.dart';
import '../../core/theme/app_dimens.dart';
import '../../core/theme/app_theme.dart';
import '../../core/widgets/auth_gate.dart';
import '../../core/widgets/empty_state.dart';
import '../../core/widgets/plain_page_scaffold.dart';
import '../../core/widgets/remote_image.dart';
import '../../l10n/app_localizations.dart';
import 'account_bindings.dart';
import 'account_profile_logic.dart';
const _avatarSize = 64.0;
const _fieldGap = 14.0;
/// 个人资料编辑页。
class AccountProfilePage extends StatefulWidget {
const AccountProfilePage({super.key});
@override
State<AccountProfilePage> createState() => _AccountProfilePageState();
}
class _AccountProfilePageState extends State<AccountProfilePage> {
late final AccountProfileLogic _logic;
final _nickname = TextEditingController();
var _gender = 'unknown';
bool _formReady = false;
@override
void initState() {
super.initState();
_logic = AccountProfileBinding().create(
context.read<AccountRepository>(),
context.read<AuthController>(),
)..addListener(_syncProfile);
unawaited(_logic.load());
}
void _syncProfile() {
final profile = _logic.profile;
if (profile == null || _formReady) return;
_nickname.text = profile.nickname;
_gender = _knownGender(profile.gender);
_formReady = true;
}
@override
void dispose() {
_logic
..removeListener(_syncProfile)
..dispose();
_nickname.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final signedIn = context.watch<AuthController>().isSignedIn;
return Theme(
data: AppTheme.light(),
child: PlainPageScaffold(
pageKey: const Key('account-profile-page'),
backKey: const Key('account-profile-back'),
title: l10n.accountProfile,
child: signedIn
? ListenableBuilder(
listenable: _logic,
builder: (context, _) => _body(l10n),
)
: const AuthGatePanel(),
),
);
}
Widget _body(AppLocalizations l10n) {
if (_logic.loading && _logic.profile == null) {
return const Center(child: CircularProgressIndicator());
}
final profile = _logic.profile;
if (profile == null) {
return EmptyState(
key: const Key('account-profile-error'),
icon: Icons.cloud_off_rounded,
title: errorText(l10n, _logic.error, l10n.accountProfileLoadFailed),
actionLabel: l10n.retry,
actionKey: const Key('account-profile-retry'),
onAction: _logic.load,
);
}
return ListView(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 28),
children: [
_identity(profile, l10n),
const SizedBox(height: 16),
_editableForm(l10n),
const SizedBox(height: 18),
_readOnlyCard(profile, l10n),
],
);
}
Widget _identity(AccountProfile profile, AppLocalizations l10n) {
return Row(
children: [
ClipOval(
child: SizedBox(
width: _avatarSize,
height: _avatarSize,
child: profile.avatarUrl.isEmpty
? const ColoredBox(
color: AppColors.raised,
child: Icon(Icons.person_rounded, size: 34),
)
: RemoteImage(profile.avatarUrl),
),
),
const SizedBox(width: 14),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
profile.nickname,
style: const TextStyle(
fontSize: AppDimens.pageTitle,
fontWeight: FontWeight.w700,
),
),
const SizedBox(height: 4),
Text(
'${l10n.accountUid}: ${profile.uid}',
style: const TextStyle(
color: AppColors.muted,
fontSize: AppDimens.caption,
),
),
],
),
),
],
);
}
Widget _editableForm(AppLocalizations l10n) {
return _AccountCard(
child: Column(
children: [
_nicknameField(l10n),
const SizedBox(height: _fieldGap),
_genderField(l10n),
if (_logic.error != null) _saveError(l10n),
const SizedBox(height: 16),
_saveButton(l10n),
],
),
);
}
Widget _nicknameField(AppLocalizations l10n) => TextField(
key: const Key('account-nickname'),
controller: _nickname,
maxLength: AccountProfileLogic.maxNicknameLength,
decoration: InputDecoration(
labelText: l10n.accountNickname,
errorText: _logic.fieldError == null ? null : l10n.accountNicknameInvalid,
),
);
Widget _genderField(AppLocalizations l10n) => DropdownButtonFormField<String>(
key: const Key('account-gender'),
initialValue: _gender,
decoration: InputDecoration(labelText: l10n.accountGender),
items: [
DropdownMenuItem(value: 'male', child: Text(l10n.accountMale)),
DropdownMenuItem(value: 'female', child: Text(l10n.accountFemale)),
DropdownMenuItem(
value: 'unknown',
child: Text(l10n.accountUnknownGender),
),
],
onChanged: (value) {
if (value != null) setState(() => _gender = value);
},
);
Widget _saveError(AppLocalizations l10n) => Padding(
padding: const EdgeInsets.only(top: 8),
child: Text(
errorText(l10n, _logic.error, l10n.accountProfileSaveFailed),
style: TextStyle(
color: Theme.of(context).colorScheme.error,
fontSize: AppDimens.caption,
),
),
);
Widget _saveButton(AppLocalizations l10n) => SizedBox(
width: double.infinity,
height: 52,
child: FilledButton(
key: const Key('account-profile-save'),
onPressed: _logic.saving ? null : _save,
child: _logic.saving
? const SizedBox(
width: 18,
height: 18,
child: CircularProgressIndicator(strokeWidth: 2),
)
: Text(l10n.save),
),
);
Widget _readOnlyCard(AccountProfile profile, AppLocalizations l10n) {
return _AccountCard(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
l10n.accountReadOnlyInfo,
style: const TextStyle(
fontSize: AppDimens.listTitle,
fontWeight: FontWeight.w700,
),
),
const SizedBox(height: 10),
_info(l10n.accountMaskedAccount, profile.maskedTarget),
_info(l10n.accountCountry, profile.countryCode),
_info(l10n.accountCurrency, profile.currency),
_info(l10n.accountStatus, profile.status),
_info(l10n.accountLanguage, profile.language),
_info(l10n.accountTimezone, profile.timezone),
_info(l10n.accountRegion, profile.regionCode),
_info(l10n.accountVipLevel, '${profile.vipLevel}'),
if (profile.createdAt != null)
_info(
l10n.accountCreatedAt,
formatLocalizedDateTime(context, profile.createdAt!),
),
if (profile.lastLoginAt != null)
_info(
l10n.accountLastLogin,
formatLocalizedDateTime(context, profile.lastLoginAt!),
),
],
),
);
}
Widget _info(String label, String value) => Padding(
padding: const EdgeInsets.symmetric(vertical: 5),
child: Row(
children: [
Expanded(
child: Text(
label,
style: const TextStyle(
color: AppColors.muted,
fontSize: AppDimens.caption,
),
),
),
Flexible(
child: Text(
value.isEmpty ? '-' : value,
textAlign: TextAlign.right,
style: const TextStyle(fontSize: AppDimens.body),
),
),
],
),
);
Future<void> _save() async {
final ok = await _logic.save(nickname: _nickname.text, gender: _gender);
if (!ok || !mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(AppLocalizations.of(context)!.accountSaved)),
);
}
String _knownGender(String value) =>
const {'male', 'female', 'unknown'}.contains(value) ? value : 'unknown';
}
class _AccountCard extends StatelessWidget {
const _AccountCard({required this.child});
final Widget child;
@override
Widget build(BuildContext context) => Container(
padding: const EdgeInsets.all(AppDimens.cardPadding),
decoration: BoxDecoration(
color: AppColors.card,
borderRadius: BorderRadius.circular(AppDimens.cardRadius),
border: Border.all(color: AppColors.outlineVariantDark),
),
child: child,
);
}