trader_settings_screen.dart 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708
  1. import 'package:flutter/material.dart';
  2. import 'package:flutter/services.dart';
  3. import 'package:image_picker/image_picker.dart';
  4. import 'package:flutter_riverpod/flutter_riverpod.dart';
  5. import 'package:go_router/go_router.dart';
  6. import '../../../core/l10n/app_localizations.dart';
  7. import '../../../core/theme/app_colors.dart';
  8. import '../../../core/utils/avatar_urls.dart';
  9. import '../../../core/utils/dialog_utils.dart';
  10. import '../../../core/utils/top_toast.dart';
  11. import '../../../data/repositories/copy_trading_repository.dart';
  12. import '../../../providers/profile_provider.dart';
  13. class TraderSettingsScreen extends ConsumerStatefulWidget {
  14. const TraderSettingsScreen({super.key});
  15. @override
  16. ConsumerState<TraderSettingsScreen> createState() =>
  17. _TraderSettingsScreenState();
  18. }
  19. class _TraderSettingsScreenState extends ConsumerState<TraderSettingsScreen> {
  20. bool _loading = true;
  21. bool _saving = false;
  22. String _traderId = '';
  23. String? _avatarUrl;
  24. late TextEditingController _nicknameCtrl;
  25. late TextEditingController _descCtrl;
  26. // 所有可选标签
  27. List<Map<String, dynamic>> _allTags = [];
  28. // 已选标签 id 集合
  29. Set<String> _selectedTagIds = {};
  30. static const int _maxTags = 4;
  31. // 所有可选合约(固定列表,与安卓一致)
  32. static const List<String> _allSymbols = ['BTC/USDT', 'ETH/USDT'];
  33. // 已选合约(symbol 名称集合)
  34. Set<String> _selectedSymbols = {};
  35. // 初始合约(用于判断是否变更)
  36. Set<String> _initialSymbols = {};
  37. @override
  38. void initState() {
  39. super.initState();
  40. _nicknameCtrl = TextEditingController();
  41. _descCtrl = TextEditingController();
  42. _loadAll();
  43. }
  44. @override
  45. void dispose() {
  46. _nicknameCtrl.dispose();
  47. _descCtrl.dispose();
  48. super.dispose();
  49. }
  50. Future<void> _loadAll() async {
  51. setState(() => _loading = true);
  52. try {
  53. final repo = ref.read(copyTradingRepositoryProvider);
  54. // 并行: 个人信息 + 所有标签 + 已选标签
  55. final results = await Future.wait([
  56. repo.getFollowerInfo(), // [0]
  57. repo.getAllTags(), // [1]
  58. repo.getMyTags(), // [2]
  59. ]);
  60. final info = results[0] as Map<String, dynamic>?;
  61. final allTags = results[1] as List<Map<String, dynamic>>;
  62. final myTags = results[2] as List<Map<String, dynamic>>;
  63. _traderId = info?['id']?.toString() ?? '';
  64. _avatarUrl = info != null ? resolvedAvatarUrlFromRecord(info) : null;
  65. _nicknameCtrl.text = info?['nickname']?.toString() ?? '';
  66. _descCtrl.text = info?['description']?.toString() ?? '';
  67. _allTags = allTags;
  68. _selectedTagIds = myTags.map((t) => t['id']?.toString() ?? '').toSet();
  69. // 加载已选合约(API 只返回已选的,全量列表用固定 _allSymbols)
  70. if (_traderId.isNotEmpty) {
  71. final symbols = await repo.getTraderSymbols(_traderId);
  72. _selectedSymbols = symbols
  73. .map((s) => s['symbol']?.toString() ?? '')
  74. .where((s) => s.isNotEmpty)
  75. .toSet();
  76. _initialSymbols = Set.from(_selectedSymbols);
  77. }
  78. if (context.mounted) setState(() => _loading = false);
  79. } catch (e) {
  80. if (context.mounted) setState(() => _loading = false);
  81. }
  82. }
  83. Future<void> _save() async {
  84. if (!context.mounted) return;
  85. setState(() => _saving = true);
  86. try {
  87. final repo = ref.read(copyTradingRepositoryProvider);
  88. final nickname = _nicknameCtrl.text.trim();
  89. final desc = _descCtrl.text.trim();
  90. // 顺序执行,避免并行时部分成功部分失败导致 UI 混乱
  91. await repo.updateTraderProfile(nickname: nickname, description: desc);
  92. await repo.updateTraderTags(_selectedTagIds.toList());
  93. // 仅当合约有变更时才调用(有跟随者时后台会拒绝)
  94. final symbolsChanged = !_selectedSymbols.containsAll(_initialSymbols) ||
  95. !_initialSymbols.containsAll(_selectedSymbols);
  96. if (symbolsChanged) {
  97. await repo.updateTraderSymbols(_selectedSymbols.toList());
  98. }
  99. if (context.mounted) {
  100. showTopToast(context,
  101. message: AppLocalizations.of(context)!.savedSuccess,
  102. backgroundColor: const Color(0xFF2ECC71));
  103. context.pop();
  104. }
  105. } catch (e) {
  106. if (context.mounted) {
  107. showTopToast(context, message: extractErrorMessage(e));
  108. }
  109. } finally {
  110. if (context.mounted) setState(() => _saving = false);
  111. }
  112. }
  113. Future<void> _pickAvatar() async {
  114. final l10nSheet = AppLocalizations.of(context)!;
  115. final source = await showModalBottomSheet<ImageSource>(
  116. context: context,
  117. useRootNavigator: true,
  118. builder: (sheetCtx) => SafeArea(
  119. child: Column(
  120. mainAxisSize: MainAxisSize.min,
  121. children: [
  122. GestureDetector(
  123. onTap: () => Navigator.pop(sheetCtx, ImageSource.camera),
  124. child: Padding(
  125. padding:
  126. const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
  127. child: Row(children: [
  128. const Icon(Icons.camera_alt_outlined),
  129. const SizedBox(width: 16),
  130. Text(l10nSheet.takePhoto)
  131. ]),
  132. ),
  133. ),
  134. GestureDetector(
  135. onTap: () => Navigator.pop(sheetCtx, ImageSource.gallery),
  136. child: Padding(
  137. padding:
  138. const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
  139. child: Row(children: [
  140. const Icon(Icons.photo_library_outlined),
  141. const SizedBox(width: 16),
  142. Text(l10nSheet.chooseFromAlbum)
  143. ]),
  144. ),
  145. ),
  146. GestureDetector(
  147. onTap: () => Navigator.pop(sheetCtx),
  148. child: Padding(
  149. padding:
  150. const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
  151. child: Row(children: [
  152. const Icon(Icons.close),
  153. const SizedBox(width: 16),
  154. Text(l10nSheet.cancelLabel)
  155. ]),
  156. ),
  157. ),
  158. ],
  159. ),
  160. ),
  161. );
  162. if (source == null || !context.mounted) return;
  163. final picker = ImagePicker();
  164. final XFile? file = await picker.pickImage(
  165. source: source,
  166. maxWidth: 800,
  167. maxHeight: 800,
  168. imageQuality: 85,
  169. );
  170. if (file == null || !context.mounted) return;
  171. setState(() => _saving = true);
  172. try {
  173. final url =
  174. await ref.read(copyTradingRepositoryProvider).updateAvatar(file.path);
  175. if (context.mounted && url != null) {
  176. setState(() {
  177. _avatarUrl = normalizeAvatarHttpUrl(url) ?? url;
  178. });
  179. ref.invalidate(profileProvider);
  180. showTopToast(context,
  181. message: AppLocalizations.of(context)!.avatarUpdated,
  182. backgroundColor: const Color(0xFF2ECC71));
  183. }
  184. } catch (e) {
  185. if (context.mounted)
  186. showTopToast(context, message: extractErrorMessage(e));
  187. } finally {
  188. if (context.mounted) setState(() => _saving = false);
  189. }
  190. }
  191. Future<void> _cancelTrader() async {
  192. final l10n = AppLocalizations.of(context)!;
  193. final confirm = await showDialog<bool>(
  194. context: context,
  195. builder: (ctx) {
  196. final cs = Theme.of(ctx).colorScheme;
  197. final isDark = Theme.of(ctx).brightness == Brightness.dark;
  198. return AlertDialog(
  199. backgroundColor: isDark ? const Color(0xFF1E1E1E) : Colors.white,
  200. shape:
  201. RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)),
  202. title: Text(
  203. l10n.confirmCancelTitle,
  204. style: TextStyle(
  205. color: cs.onSurface, fontSize: 17, fontWeight: FontWeight.w600),
  206. ),
  207. content: Text(
  208. l10n.confirmCancelTraderMsg,
  209. style: TextStyle(
  210. color: cs.onSurface.withAlpha(180), fontSize: 14, height: 1.5),
  211. ),
  212. actions: [
  213. TextButton(
  214. onPressed: () => Navigator.pop(ctx, false),
  215. child: Text(l10n.cancelLabel,
  216. style: TextStyle(color: cs.onSurface.withAlpha(153))),
  217. ),
  218. TextButton(
  219. onPressed: () => Navigator.pop(ctx, true),
  220. child:
  221. Text(l10n.confirm, style: const TextStyle(color: Colors.red)),
  222. ),
  223. ],
  224. );
  225. },
  226. );
  227. if (confirm != true) return;
  228. try {
  229. await ref.read(copyTradingRepositoryProvider).cancelTraderQualification();
  230. if (context.mounted) {
  231. showTopToast(context,
  232. message: AppLocalizations.of(context)!.applicationSubmitted,
  233. backgroundColor: const Color(0xFF2ECC71));
  234. context.pop();
  235. }
  236. } catch (e) {
  237. if (context.mounted) {
  238. showTopToast(context, message: extractErrorMessage(e));
  239. }
  240. }
  241. }
  242. void _toggleTag(String id) {
  243. setState(() {
  244. if (_selectedTagIds.contains(id)) {
  245. _selectedTagIds.remove(id);
  246. } else {
  247. if (_selectedTagIds.length >= _maxTags) return;
  248. _selectedTagIds.add(id);
  249. }
  250. });
  251. }
  252. void _toggleSymbol(String symbol) {
  253. setState(() {
  254. if (_selectedSymbols.contains(symbol)) {
  255. _selectedSymbols.remove(symbol);
  256. } else {
  257. _selectedSymbols.add(symbol);
  258. }
  259. });
  260. }
  261. @override
  262. Widget build(BuildContext context) {
  263. final cs = Theme.of(context).colorScheme;
  264. final nickname =
  265. _nicknameCtrl.text.isEmpty ? 'T' : _nicknameCtrl.text[0].toUpperCase();
  266. return Scaffold(
  267. appBar: AppBar(
  268. leading: IconButton(
  269. icon: const Icon(Icons.arrow_back_ios, size: 18),
  270. onPressed: () => context.pop(),
  271. ),
  272. title: Text(AppLocalizations.of(context)!.tradingSettings,
  273. style: const TextStyle(fontSize: 17, fontWeight: FontWeight.w600)),
  274. ),
  275. body: _loading
  276. ? const Center(child: CircularProgressIndicator())
  277. : SingleChildScrollView(
  278. padding: const EdgeInsets.symmetric(horizontal: 20),
  279. child: Column(
  280. crossAxisAlignment: CrossAxisAlignment.start,
  281. children: [
  282. const SizedBox(height: 24),
  283. // ── 头像 ──────────────────────────────────
  284. Center(
  285. child: GestureDetector(
  286. onTap: _pickAvatar,
  287. child: Stack(
  288. children: [
  289. ClipRRect(
  290. borderRadius: BorderRadius.circular(20),
  291. child:
  292. (_avatarUrl != null && _avatarUrl!.isNotEmpty)
  293. ? Image.network(
  294. _avatarUrl!,
  295. width: 80,
  296. height: 80,
  297. fit: BoxFit.cover,
  298. errorBuilder: (_, __, ___) =>
  299. _DefaultAvatar(letter: nickname),
  300. )
  301. : _DefaultAvatar(letter: nickname),
  302. ),
  303. Positioned(
  304. bottom: 2,
  305. right: 2,
  306. child: Container(
  307. width: 22,
  308. height: 22,
  309. decoration: BoxDecoration(
  310. color: cs.surface,
  311. shape: BoxShape.circle,
  312. boxShadow: [
  313. BoxShadow(
  314. color: Colors.black.withAlpha(30),
  315. blurRadius: 4)
  316. ],
  317. ),
  318. child: Icon(Icons.edit_outlined,
  319. size: 13, color: cs.onSurface.withAlpha(180)),
  320. ),
  321. ),
  322. ],
  323. ),
  324. ),
  325. ),
  326. const SizedBox(height: 28),
  327. // ── 带单昵称 ──────────────────────────────
  328. _SectionLabel(
  329. label: AppLocalizations.of(context)!.tradingNickname),
  330. const SizedBox(height: 8),
  331. _InputField(
  332. controller: _nicknameCtrl,
  333. hint: AppLocalizations.of(context)!.enterNickname,
  334. maxLength: 10),
  335. const SizedBox(height: 20),
  336. // ── 个人签名 ──────────────────────────────
  337. _SectionLabel(
  338. label: AppLocalizations.of(context)!.personalBio),
  339. const SizedBox(height: 8),
  340. _SignatureField(controller: _descCtrl),
  341. const SizedBox(height: 20),
  342. // ── 标签 ─────────────────────────────────
  343. Row(
  344. children: [
  345. _SectionLabel(
  346. label: AppLocalizations.of(context)!.tagsLabel),
  347. const Spacer(),
  348. Text(
  349. AppLocalizations.of(context)!.maxTagsHint(
  350. _maxTags.toString(),
  351. _selectedTagIds.length.toString()),
  352. style: TextStyle(
  353. color: cs.onSurface.withAlpha(120), fontSize: 12),
  354. ),
  355. ],
  356. ),
  357. const SizedBox(height: 10),
  358. _TagsWrap(
  359. tags: _allTags,
  360. selectedIds: _selectedTagIds,
  361. onToggle: _toggleTag,
  362. ),
  363. const SizedBox(height: 20),
  364. // ── 带单合约 ──────────────────────────────
  365. _SectionLabel(
  366. label: AppLocalizations.of(context)!.tradingContracts),
  367. const SizedBox(height: 10),
  368. _SymbolsWrap(
  369. allSymbols: _allSymbols,
  370. selected: _selectedSymbols,
  371. onToggle: _toggleSymbol,
  372. ),
  373. const SizedBox(height: 32),
  374. // ── 保存按钮 ──────────────────────────────
  375. SizedBox(
  376. width: double.infinity,
  377. height: 52,
  378. child: ElevatedButton(
  379. onPressed: _saving ? null : _save,
  380. style: ElevatedButton.styleFrom(
  381. backgroundColor: AppColors.brand,
  382. foregroundColor: Colors.black,
  383. disabledBackgroundColor: AppColors.brand.withAlpha(80),
  384. shape: RoundedRectangleBorder(
  385. borderRadius: BorderRadius.circular(26)),
  386. elevation: 0,
  387. ),
  388. child: _saving
  389. ? SizedBox(
  390. width: 20,
  391. height: 20,
  392. child: CircularProgressIndicator(
  393. color: Colors.black, strokeWidth: 2),
  394. )
  395. : Text(AppLocalizations.of(context)!.saveLabel,
  396. style: const TextStyle(
  397. fontSize: 16, fontWeight: FontWeight.w600)),
  398. ),
  399. ),
  400. const SizedBox(height: 16),
  401. // ── 取消资格 ─────────────────────────────
  402. Center(
  403. child: GestureDetector(
  404. onTap: _cancelTrader,
  405. child: Text(
  406. AppLocalizations.of(context)!.cancelTraderQualify,
  407. style: TextStyle(
  408. color: cs.onSurface.withAlpha(153),
  409. fontSize: 13,
  410. decoration: TextDecoration.underline,
  411. decorationColor: cs.onSurface.withAlpha(100),
  412. ),
  413. ),
  414. ),
  415. ),
  416. const SizedBox(height: 32),
  417. ],
  418. ),
  419. ),
  420. );
  421. }
  422. }
  423. // ── 通用子组件 ────────────────────────────────────────────
  424. class _SectionLabel extends StatelessWidget {
  425. const _SectionLabel({required this.label});
  426. final String label;
  427. @override
  428. Widget build(BuildContext context) {
  429. return Text(
  430. label,
  431. style: TextStyle(
  432. color: Theme.of(context).colorScheme.onSurface,
  433. fontSize: 14,
  434. fontWeight: FontWeight.w600,
  435. ),
  436. );
  437. }
  438. }
  439. class _InputField extends StatelessWidget {
  440. const _InputField(
  441. {required this.controller, required this.hint, this.maxLength});
  442. final TextEditingController controller;
  443. final String hint;
  444. final int? maxLength;
  445. @override
  446. Widget build(BuildContext context) {
  447. final cs = Theme.of(context).colorScheme;
  448. return Container(
  449. decoration: BoxDecoration(
  450. border: Border.all(color: cs.outline.withAlpha(60)),
  451. borderRadius: BorderRadius.circular(8),
  452. ),
  453. child: TextField(
  454. controller: controller,
  455. maxLength: maxLength,
  456. maxLengthEnforcement: MaxLengthEnforcement.enforced,
  457. style: TextStyle(color: cs.onSurface, fontSize: 14),
  458. decoration: InputDecoration(
  459. hintText: hint,
  460. hintStyle:
  461. TextStyle(color: cs.onSurface.withAlpha(100), fontSize: 14),
  462. contentPadding:
  463. const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
  464. border: InputBorder.none,
  465. counterText: '',
  466. ),
  467. ),
  468. );
  469. }
  470. }
  471. class _SignatureField extends StatefulWidget {
  472. const _SignatureField({required this.controller});
  473. final TextEditingController controller;
  474. @override
  475. State<_SignatureField> createState() => _SignatureFieldState();
  476. }
  477. class _SignatureFieldState extends State<_SignatureField> {
  478. static const int _maxLen = 20;
  479. void _onControllerChanged() => setState(() {});
  480. @override
  481. void initState() {
  482. super.initState();
  483. widget.controller.addListener(_onControllerChanged);
  484. }
  485. @override
  486. void dispose() {
  487. widget.controller.removeListener(_onControllerChanged);
  488. super.dispose();
  489. }
  490. @override
  491. Widget build(BuildContext context) {
  492. final cs = Theme.of(context).colorScheme;
  493. final count = widget.controller.text.length;
  494. return Container(
  495. decoration: BoxDecoration(
  496. border: Border.all(color: cs.outline.withAlpha(60)),
  497. borderRadius: BorderRadius.circular(8),
  498. ),
  499. child: Column(
  500. children: [
  501. TextField(
  502. controller: widget.controller,
  503. maxLines: 4,
  504. maxLength: _maxLen,
  505. style: TextStyle(color: cs.onSurface, fontSize: 14),
  506. decoration: InputDecoration(
  507. hintText: AppLocalizations.of(context)!.bioHint,
  508. hintStyle:
  509. TextStyle(color: cs.onSurface.withAlpha(100), fontSize: 13),
  510. contentPadding: const EdgeInsets.fromLTRB(14, 12, 14, 4),
  511. border: InputBorder.none,
  512. counterText: '',
  513. ),
  514. ),
  515. Padding(
  516. padding: const EdgeInsets.fromLTRB(0, 0, 12, 8),
  517. child: Align(
  518. alignment: Alignment.centerRight,
  519. child: Text(
  520. '$count/$_maxLen',
  521. style:
  522. TextStyle(color: cs.onSurface.withAlpha(100), fontSize: 12),
  523. ),
  524. ),
  525. ),
  526. ],
  527. ),
  528. );
  529. }
  530. }
  531. class _TagsWrap extends StatelessWidget {
  532. const _TagsWrap(
  533. {required this.tags, required this.selectedIds, required this.onToggle});
  534. final List<Map<String, dynamic>> tags;
  535. final Set<String> selectedIds;
  536. final void Function(String id) onToggle;
  537. @override
  538. Widget build(BuildContext context) {
  539. final cs = Theme.of(context).colorScheme;
  540. final l10n = AppLocalizations.of(context)!;
  541. // 如果 API 返回空则用默认标签
  542. final displayTags = tags.isNotEmpty
  543. ? tags
  544. : [
  545. {'id': '1', 'name': l10n.tagShortTerm},
  546. {'id': '2', 'name': l10n.tagMidLong},
  547. {'id': '3', 'name': l10n.tagConservative},
  548. {'id': '4', 'name': l10n.tagAggressive},
  549. {'id': '5', 'name': l10n.tagHighLeverage},
  550. {'id': '6', 'name': l10n.tagLowLeverage},
  551. ];
  552. return Wrap(
  553. spacing: 10,
  554. runSpacing: 10,
  555. children: displayTags.map((tag) {
  556. final id = tag['id']?.toString() ?? '';
  557. final name = tag['name']?.toString() ?? '';
  558. final selected = selectedIds.contains(id);
  559. return GestureDetector(
  560. onTap: () => onToggle(id),
  561. child: Container(
  562. padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 8),
  563. decoration: BoxDecoration(
  564. color: selected ? AppColors.brand : Colors.transparent,
  565. border: Border.all(
  566. color: selected ? AppColors.brand : cs.outline.withAlpha(80)),
  567. borderRadius: BorderRadius.circular(20),
  568. ),
  569. child: Text(
  570. name,
  571. style: TextStyle(
  572. color: selected ? Colors.black : cs.onSurface,
  573. fontSize: 13,
  574. fontWeight: selected ? FontWeight.w600 : FontWeight.normal,
  575. ),
  576. ),
  577. ),
  578. );
  579. }).toList(),
  580. );
  581. }
  582. }
  583. class _SymbolsWrap extends StatelessWidget {
  584. const _SymbolsWrap(
  585. {required this.allSymbols,
  586. required this.selected,
  587. required this.onToggle});
  588. final List<String> allSymbols;
  589. final Set<String> selected;
  590. final void Function(String symbol) onToggle;
  591. @override
  592. Widget build(BuildContext context) {
  593. final cs = Theme.of(context).colorScheme;
  594. return Wrap(
  595. spacing: 10,
  596. runSpacing: 10,
  597. children: allSymbols.map((symbol) {
  598. final isSelected = selected.contains(symbol);
  599. return GestureDetector(
  600. onTap: () => onToggle(symbol),
  601. child: Container(
  602. padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
  603. decoration: BoxDecoration(
  604. color: isSelected ? AppColors.brand : Colors.transparent,
  605. border: Border.all(
  606. color: isSelected ? AppColors.brand : cs.outline.withAlpha(80),
  607. ),
  608. borderRadius: BorderRadius.circular(20),
  609. ),
  610. child: Text(
  611. symbol,
  612. style: TextStyle(
  613. color: isSelected ? Colors.black : cs.onSurface,
  614. fontSize: 13,
  615. fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal,
  616. ),
  617. ),
  618. ),
  619. );
  620. }).toList(),
  621. );
  622. }
  623. }
  624. class _DefaultAvatar extends StatelessWidget {
  625. const _DefaultAvatar({required this.letter});
  626. final String letter;
  627. @override
  628. Widget build(BuildContext context) {
  629. return Container(
  630. width: 80,
  631. height: 80,
  632. decoration: BoxDecoration(
  633. gradient: const LinearGradient(
  634. colors: [Color(0xFF5B7BE8), Color(0xFF7B5EA7)],
  635. begin: Alignment.topLeft,
  636. end: Alignment.bottomRight,
  637. ),
  638. borderRadius: BorderRadius.circular(20),
  639. ),
  640. child: Center(
  641. child: Text(letter,
  642. style: const TextStyle(
  643. color: Colors.white,
  644. fontSize: 32,
  645. fontWeight: FontWeight.w700)),
  646. ),
  647. );
  648. }
  649. }