Surface/lib/screens/wallet.dart
2025-01-30 14:58:06 +08:00

280 lines
8.6 KiB
Dart

import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:material_symbols_icons/material_symbols_icons.dart';
import 'package:provider/provider.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:surface/providers/sn_network.dart';
import 'package:surface/types/wallet.dart';
import 'package:surface/widgets/dialog.dart';
import 'package:surface/widgets/loading_indicator.dart';
import 'package:surface/widgets/navigation/app_scaffold.dart';
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
class WalletScreen extends StatefulWidget {
const WalletScreen({super.key});
@override
State<WalletScreen> createState() => _WalletScreenState();
}
class _WalletScreenState extends State<WalletScreen> {
bool _isBusy = false;
SnWallet? _wallet;
Future<void> _fetchWallet() async {
try {
setState(() => _isBusy = true);
final sn = context.read<SnNetworkProvider>();
final resp = await sn.client.get('/cgi/wa/wallets/me');
_wallet = SnWallet.fromJson(resp.data);
} catch (err) {
if (!mounted) return;
context.showErrorDialog(err);
} finally {
setState(() => _isBusy = false);
}
}
@override
void initState() {
super.initState();
_fetchWallet();
}
@override
Widget build(BuildContext context) {
return AppScaffold(
appBar: AppBar(
leading: PageBackButton(),
title: Text('screenAccountWallet').tr(),
),
body: Column(
children: [
LoadingIndicator(isActive: _isBusy),
if (_wallet == null)
Expanded(
child: _CreateWalletWidget(
onCreate: () {
_fetchWallet();
},
),
)
else
Card(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
CircleAvatar(
radius: 28,
child: Icon(Symbols.wallet, size: 28),
),
const Gap(12),
SizedBox(width: double.infinity),
Text(
NumberFormat.compactCurrency(
locale: EasyLocalization.of(context)!.currentLocale.toString(),
symbol: '${'walletCurrencyShort'.tr()} ',
decimalDigits: 2,
).format(double.parse(_wallet!.balance)),
style: Theme.of(context).textTheme.titleLarge,
),
Text('walletCurrency'.plural(double.parse(_wallet!.balance))),
],
).padding(horizontal: 20, vertical: 24),
).padding(horizontal: 8, top: 16, bottom: 4),
if (_wallet != null) Expanded(child: _WalletTransactionList(myself: _wallet!)),
],
),
);
}
}
class _WalletTransactionList extends StatefulWidget {
final SnWallet myself;
const _WalletTransactionList({required this.myself});
@override
State<_WalletTransactionList> createState() => _WalletTransactionListState();
}
class _WalletTransactionListState extends State<_WalletTransactionList> {
bool _isBusy = false;
int? _totalCount;
final List<SnTransaction> _transactions = List.empty(growable: true);
Future<void> _fetchTransactions() async {
try {
setState(() => _isBusy = true);
final sn = context.read<SnNetworkProvider>();
final resp = await sn.client.get('/cgi/wa/transactions/me', queryParameters: {
'take': 10,
'offset': _transactions.length,
});
_totalCount = resp.data['count'];
_transactions.addAll(
resp.data['data']?.map((e) => SnTransaction.fromJson(e)).cast<SnTransaction>() ?? [],
);
} catch (err) {
if (!mounted) return;
context.showErrorDialog(err);
} finally {
setState(() => _isBusy = false);
}
}
@override
void initState() {
super.initState();
_fetchTransactions();
}
@override
Widget build(BuildContext context) {
return MediaQuery.removePadding(
context: context,
removeTop: true,
child: RefreshIndicator(
onRefresh: _fetchTransactions,
child: InfiniteList(
itemCount: _transactions.length,
isLoading: _isBusy,
hasReachedMax: _totalCount != null && _transactions.length >= _totalCount!,
onFetchData: () {
_fetchTransactions();
},
itemBuilder: (context, idx) {
final ele = _transactions[idx];
final isIncoming = ele.payeeId == widget.myself.id;
return ListTile(
leading: isIncoming ? const Icon(Symbols.call_received) : const Icon(Symbols.call_made),
title: Text(
'${isIncoming ? '+' : '-'}${ele.amount} ${'walletCurrencyShort'.tr()}',
style: TextStyle(color: isIncoming ? Colors.green : Colors.red),
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(ele.remark),
const Gap(2),
Text(
DateFormat(
null,
EasyLocalization.of(context)!.currentLocale.toString(),
).format(ele.createdAt),
style: Theme.of(context).textTheme.labelSmall,
),
],
),
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
);
},
),
),
);
}
}
class _CreateWalletWidget extends StatefulWidget {
final Function()? onCreate;
const _CreateWalletWidget({required this.onCreate});
@override
State<_CreateWalletWidget> createState() => _CreateWalletWidgetState();
}
class _CreateWalletWidgetState extends State<_CreateWalletWidget> {
bool _isBusy = false;
Future<void> _createWallet() async {
final TextEditingController passwordController = TextEditingController();
final password = await showDialog<String?>(
context: context,
builder: (ctx) => AlertDialog(
title: Text('walletCreate').tr(),
content: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text('walletCreatePassword').tr(),
const Gap(8),
TextField(
autofocus: true,
obscureText: true,
controller: passwordController,
decoration: InputDecoration(
labelText: 'fieldPassword'.tr(),
),
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.of(ctx).pop(),
child: Text('cancel').tr(),
),
TextButton(
onPressed: () {
Navigator.of(ctx).pop(passwordController.text);
},
child: Text('next').tr(),
),
],
),
);
WidgetsBinding.instance.addPostFrameCallback((_) {
passwordController.dispose();
});
if (password == null || password.isEmpty) return;
if (!mounted) return;
try {
setState(() => _isBusy = true);
final sn = context.read<SnNetworkProvider>();
await sn.client.post('/cgi/wa/wallets/me', data: {
'password': password,
});
} catch (err) {
if (!mounted) return;
context.showErrorDialog(err);
} finally {
setState(() => _isBusy = false);
}
}
@override
Widget build(BuildContext context) {
return Center(
child: Container(
constraints: const BoxConstraints(maxWidth: 380),
child: Card(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
CircleAvatar(
radius: 28,
child: Icon(Symbols.add, size: 28),
),
const Gap(12),
Text('walletCreate', style: Theme.of(context).textTheme.titleLarge).tr(),
Text('walletCreateSubtitle', style: Theme.of(context).textTheme.bodyMedium).tr(),
const Gap(8),
Align(
alignment: Alignment.centerRight,
child: TextButton(
onPressed: _isBusy ? null : () => _createWallet(),
child: Text('next').tr(),
),
),
],
).padding(horizontal: 20, vertical: 24),
),
),
);
}
}