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

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.light(),
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';
}