217 lines
6.4 KiB
Dart
217 lines
6.4 KiB
Dart
// Author: fruit.com.cn
|
|
|
|
import 'dart:async';
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:provider/provider.dart';
|
|
|
|
import '../../core/data/models/account_device.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 '../../l10n/app_localizations.dart';
|
|
import 'account_bindings.dart';
|
|
import 'account_devices_logic.dart';
|
|
|
|
const _maskVisible = 4;
|
|
|
|
/// 登录设备只读列表页。
|
|
class AccountDevicesPage extends StatefulWidget {
|
|
const AccountDevicesPage({super.key});
|
|
|
|
@override
|
|
State<AccountDevicesPage> createState() => _AccountDevicesPageState();
|
|
}
|
|
|
|
class _AccountDevicesPageState extends State<AccountDevicesPage> {
|
|
late final AccountDevicesLogic _logic;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_logic = AccountDevicesBinding().create(context.read<AccountRepository>());
|
|
unawaited(_logic.load());
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_logic.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final l10n = AppLocalizations.of(context)!;
|
|
final signedIn = context.watch<AuthController>().isSignedIn;
|
|
return Theme(
|
|
data: AppTheme.dark(),
|
|
child: PlainPageScaffold(
|
|
pageKey: const Key('account-devices-page'),
|
|
backKey: const Key('account-devices-back'),
|
|
title: l10n.accountDevices,
|
|
child: signedIn
|
|
? ListenableBuilder(
|
|
listenable: _logic,
|
|
builder: (context, _) => _body(l10n),
|
|
)
|
|
: const AuthGatePanel(),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _body(AppLocalizations l10n) {
|
|
if (_logic.loading && _logic.devices.isEmpty) {
|
|
return const Center(child: CircularProgressIndicator());
|
|
}
|
|
if (_logic.error != null && _logic.devices.isEmpty) {
|
|
return EmptyState(
|
|
key: const Key('account-devices-error'),
|
|
icon: Icons.cloud_off_rounded,
|
|
title: errorText(l10n, _logic.error, l10n.accountDevicesLoadFailed),
|
|
actionLabel: l10n.retry,
|
|
actionKey: const Key('account-devices-retry'),
|
|
onAction: _logic.load,
|
|
);
|
|
}
|
|
if (_logic.devices.isEmpty) {
|
|
return EmptyState(
|
|
key: const Key('account-devices-empty'),
|
|
icon: Icons.devices_other_rounded,
|
|
title: l10n.accountDevicesEmpty,
|
|
);
|
|
}
|
|
return ListView.separated(
|
|
padding: const EdgeInsets.fromLTRB(16, 12, 16, 28),
|
|
itemCount: _logic.devices.length,
|
|
separatorBuilder: (_, _) => const SizedBox(height: 10),
|
|
itemBuilder: (_, index) =>
|
|
_DeviceTile(device: _logic.devices[index], l10n: l10n),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _DeviceTile extends StatelessWidget {
|
|
const _DeviceTile({required this.device, required this.l10n});
|
|
|
|
final AccountDevice device;
|
|
final AppLocalizations l10n;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Container(
|
|
key: ValueKey('account-device-${device.id}'),
|
|
padding: const EdgeInsets.all(AppDimens.cardPadding),
|
|
decoration: BoxDecoration(
|
|
color: AppColors.card,
|
|
borderRadius: BorderRadius.circular(AppDimens.cardRadius),
|
|
border: Border.all(color: AppColors.outlineVariantDark),
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
const Icon(
|
|
Icons.phone_android_rounded,
|
|
color: AppColors.blueSoft,
|
|
),
|
|
const SizedBox(width: 8),
|
|
Expanded(
|
|
child: Text(
|
|
device.deviceModel.isEmpty
|
|
? device.platform
|
|
: device.deviceModel,
|
|
style: const TextStyle(
|
|
fontSize: AppDimens.listTitle,
|
|
fontWeight: FontWeight.w700,
|
|
),
|
|
),
|
|
),
|
|
_StatusChip(status: device.status, l10n: l10n),
|
|
],
|
|
),
|
|
const SizedBox(height: 10),
|
|
_line(l10n.accountPlatform, device.platform),
|
|
_line(l10n.accountDeviceId, _maskDeviceId(device.deviceId)),
|
|
if (device.firstSeenAt != null)
|
|
_line(
|
|
l10n.accountFirstSeen,
|
|
formatLocalizedDateTime(context, device.firstSeenAt!),
|
|
),
|
|
if (device.lastSeenAt != null)
|
|
_line(
|
|
l10n.accountLastActive,
|
|
formatLocalizedDateTime(context, device.lastSeenAt!),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _line(String label, String value) => Padding(
|
|
padding: const EdgeInsets.only(top: 4),
|
|
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.caption),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
class _StatusChip extends StatelessWidget {
|
|
const _StatusChip({required this.status, required this.l10n});
|
|
|
|
final String status;
|
|
final AppLocalizations l10n;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final active = status.toLowerCase() == 'active';
|
|
return Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
|
decoration: BoxDecoration(
|
|
color: active
|
|
? AppColors.gold.withValues(alpha: 0.16)
|
|
: AppColors.raised,
|
|
borderRadius: BorderRadius.circular(AppDimens.pillRadius),
|
|
),
|
|
child: Text(
|
|
active ? l10n.accountDeviceActive : l10n.accountDeviceInactive,
|
|
style: TextStyle(
|
|
color: active ? AppColors.gold : AppColors.muted,
|
|
fontSize: AppDimens.caption,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
String _maskDeviceId(String value) {
|
|
if (value.length <= _maskVisible * 2) return value;
|
|
final head = value.substring(0, _maskVisible);
|
|
final tail = value.substring(value.length - _maskVisible);
|
|
return '$head****$tail';
|
|
}
|