import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:gap/gap.dart'; import 'package:material_symbols_icons/symbols.dart'; import 'package:provider/provider.dart'; import 'package:styled_widget/styled_widget.dart'; import 'package:surface/providers/experience.dart'; import 'package:surface/providers/sn_network.dart'; import 'package:surface/types/account.dart'; import 'package:surface/widgets/dialog.dart'; import 'package:surface/widgets/loading_indicator.dart'; import 'package:surface/widgets/navigation/app_scaffold.dart'; class AccountProgramScreen extends StatefulWidget { const AccountProgramScreen({super.key}); @override State createState() => _AccountProgramScreenState(); } class _AccountProgramScreenState extends State { bool _isBusy = false; final List _programs = List.empty(growable: true); final List _programMembers = List.empty(growable: true); Future _fetchPrograms() async { _programs.clear(); setState(() => _isBusy = true); try { final sn = context.read(); final resp = await sn.client.get('/cgi/id/programs'); _programs.addAll( resp.data.map((ele) => SnProgram.fromJson(ele)).cast(), ); } catch (err) { if (!mounted) return; context.showErrorDialog(err); } finally { setState(() => _isBusy = false); } } Future _fetchProgramMembers() async { _programMembers.clear(); setState(() => _isBusy = true); try { final sn = context.read(); final resp = await sn.client.get('/cgi/id/programs/members'); _programMembers.addAll( resp.data .map((ele) => SnProgramMember.fromJson(ele)) .cast(), ); } catch (err) { if (!mounted) return; context.showErrorDialog(err); } finally { setState(() => _isBusy = false); } } @override void initState() { super.initState(); _fetchPrograms(); _fetchProgramMembers(); } @override Widget build(BuildContext context) { return AppScaffold( appBar: AppBar( title: Text('accountProgram').tr(), ), body: Column( children: [ LoadingIndicator(isActive: _isBusy), Expanded( child: ListView.builder( padding: EdgeInsets.zero, itemCount: _programs.length, itemBuilder: (context, idx) { final ele = _programs[idx]; return Card( child: InkWell( borderRadius: BorderRadius.all(Radius.circular(8)), onTap: () { showModalBottomSheet( context: context, builder: (context) => _ProgramJoinPopup( program: ele, isJoined: _programMembers .any((ele) => ele.programId == ele.id), ), ).then((value) { _fetchProgramMembers(); }); }, child: Column( children: [ if (ele.appearance['banner'] != null) AspectRatio( aspectRatio: 16 / 5, child: ClipRRect( borderRadius: BorderRadius.circular(8), child: Container( color: Theme.of(context) .colorScheme .surfaceVariant, child: Image.network( ele.appearance['banner'], color: Theme.of(context) .colorScheme .onSurfaceVariant, ), ), ), ), Padding( padding: const EdgeInsets.all(16), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( ele.name, style: Theme.of(context) .textTheme .titleMedium, ).bold(), Text( ele.description, maxLines: 3, overflow: TextOverflow.ellipsis, ), if (_programMembers .any((ele) => ele.programId == ele.id)) Text('accountProgramAlreadyJoined'.tr()) .opacity(0.75), ], ), ), ], ), ), ], ), ), ).padding(horizontal: 8); }, ), ), ], ), ); } } class _ProgramJoinPopup extends StatefulWidget { final SnProgram program; final bool isJoined; const _ProgramJoinPopup({required this.program, required this.isJoined}); @override State<_ProgramJoinPopup> createState() => _ProgramJoinPopupState(); } class _ProgramJoinPopupState extends State<_ProgramJoinPopup> { bool _isBusy = false; Future _joinProgram() async { setState(() => _isBusy = true); try { final sn = context.read(); await sn.client.post('/cgi/id/programs/${widget.program.id}'); if (!mounted) return; Navigator.pop(context, true); context.showSnackbar('accountProgramJoined'.tr()); } catch (err) { if (!mounted) return; context.showErrorDialog(err); } finally { setState(() => _isBusy = false); } } Future _leaveProgram() async { setState(() => _isBusy = true); try { final sn = context.read(); await sn.client.delete('/cgi/id/programs/${widget.program.id}'); if (!mounted) return; Navigator.pop(context, true); context.showSnackbar('accountProgramLeft'.tr()); } catch (err) { if (!mounted) return; context.showErrorDialog(err); } finally { setState(() => _isBusy = false); } } @override Widget build(BuildContext context) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ const Icon(Symbols.add, size: 24), const Gap(16), Text( 'accountProgramJoin', style: Theme.of(context).textTheme.titleLarge, ).tr(), ], ).padding(horizontal: 20, top: 16, bottom: 12), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ if (widget.program.appearance['banner'] != null) AspectRatio( aspectRatio: 16 / 5, child: ClipRRect( borderRadius: BorderRadius.circular(8), child: Container( color: Theme.of(context).colorScheme.surfaceVariant, child: Image.network( widget.program.appearance['banner'], color: Theme.of(context).colorScheme.onSurfaceVariant, ), ), ), ).padding(bottom: 12), Text( widget.program.name, style: Theme.of(context).textTheme.titleMedium, ).bold(), Text( widget.program.description, maxLines: 3, overflow: TextOverflow.ellipsis, ), const Gap(8), Text( 'accountProgramJoinRequirements', style: Theme.of(context).textTheme.titleMedium, ).tr().bold(), Text('≥EXP ${widget.program.expRequirement}'), Text('≥Lv${getLevelFromExp(widget.program.expRequirement)}'), const Gap(8), Text( 'accountProgramJoinPricing', style: Theme.of(context).textTheme.titleMedium, ).tr().bold(), Text('walletCurrency${widget.program.price['currency'].toString().capitalize().replaceFirst('Normal', '')}') .plural(widget.program.price['amount'].toDouble()), Text('accountProgramJoinPricingHint').tr().opacity(0.75), const Gap(8), if (widget.isJoined) Text('accountProgramLeaveHint') .tr() .opacity(0.75) .padding(bottom: 8), if (!widget.isJoined) ElevatedButton( onPressed: _isBusy ? null : _joinProgram, child: Text('join').tr(), ) else ElevatedButton( onPressed: _isBusy ? null : _leaveProgram, child: Text('leave').tr(), ), ], ).padding(horizontal: 24), ], ); } }