568c49bb3a
Changelog: iOS-style surfaces, alerts and sheets; clearer TV focus
230 lines
6.8 KiB
Dart
230 lines
6.8 KiB
Dart
import 'package:fl_clash/common/common.dart';
|
|
import 'package:fl_clash/common/ip_quality.dart';
|
|
import 'package:fl_clash/models/models.dart';
|
|
import 'package:fl_clash/providers/config.dart';
|
|
import 'package:fl_clash/providers/ip_quality.dart';
|
|
import 'package:fl_clash/ui/icons/icons.dart';
|
|
import 'package:fl_clash/widgets/widgets.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:material_ui/material_ui.dart';
|
|
|
|
import 'ip_quality_text.dart';
|
|
import 'labels.dart';
|
|
|
|
Future<void> showIpQualitySheet(BuildContext context, {required String ip}) {
|
|
if (isSheetPage(context)) {
|
|
return Navigator.of(
|
|
context,
|
|
).push(PagedSheetRoute(builder: (_) => _IpQualitySheet(ip: ip)));
|
|
}
|
|
return showSheet<void>(
|
|
context: context,
|
|
builder: (_) => _IpQualitySheet(ip: ip),
|
|
);
|
|
}
|
|
|
|
class _IpQualitySheet extends ConsumerWidget {
|
|
const _IpQualitySheet({required this.ip});
|
|
|
|
final String ip;
|
|
|
|
@override
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
final localizations = context.appLocalizations;
|
|
final isLoading = ref.watch(
|
|
ipQualityProvider(ip).select((result) => result.isLoading),
|
|
);
|
|
return CommonScaffold(
|
|
title: localizations.outboundIp,
|
|
actions: [
|
|
AppBarActionButton(
|
|
data: IconButtonData(
|
|
glyph: AppGlyphs.refresh,
|
|
tooltip: localizations.ipQualityRetry,
|
|
isLoading: isLoading,
|
|
onPressed: () => ref.invalidate(ipQualityProvider(ip)),
|
|
),
|
|
),
|
|
],
|
|
body: _IpQualityDetail(ip: ip),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _HideIpItem extends ConsumerWidget {
|
|
const _HideIpItem();
|
|
|
|
@override
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
final hideIp = ref.watch(
|
|
appSettingProvider.select((state) => state.hideIp),
|
|
);
|
|
void update(bool value) => ref
|
|
.read(appSettingProvider.notifier)
|
|
.update((state) => state.copyWith(hideIp: value));
|
|
return DecorationListItem(
|
|
title: Text(context.appLocalizations.hideIp),
|
|
onPressed: () => update(!hideIp),
|
|
trailing: Switch(value: hideIp, onChanged: update),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _IpQualityDetail extends ConsumerWidget {
|
|
const _IpQualityDetail({required this.ip});
|
|
|
|
/// The optical edge of the xl-radius cards, as in the memory sheet.
|
|
static const _tipInset = 5.0;
|
|
|
|
final String ip;
|
|
|
|
@override
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
final localizations = context.appLocalizations;
|
|
final result = ref.watch(ipQualityProvider(ip));
|
|
final quality = result.value;
|
|
final error = result.error;
|
|
final failed = quality == null && !result.isLoading;
|
|
return ListView(
|
|
shrinkWrap: true,
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 16,
|
|
).copyWith(bottom: 20, top: context.contentTopPadding),
|
|
children: [
|
|
Padding(
|
|
padding: const EdgeInsets.fromLTRB(_tipInset, 8, _tipInset, 0),
|
|
child: Text(
|
|
localizations.detectionTip,
|
|
style: AppTextStyle.body
|
|
.of(context)
|
|
.copyWith(color: context.colorScheme.onSurfaceVariant),
|
|
),
|
|
),
|
|
generateSection(
|
|
title: localizations.basicInfo,
|
|
items: [
|
|
DetailRow(
|
|
title: localizations.ipAddress,
|
|
copyText: ip,
|
|
value: IpQualityText(ip: ip),
|
|
),
|
|
if (failed)
|
|
DetailRow(
|
|
title: localizations.ipType,
|
|
value: Text(
|
|
localizations.ipQualityFailed,
|
|
style: TextStyle(color: context.colorScheme.error),
|
|
),
|
|
)
|
|
else
|
|
..._qualityRows(context, quality),
|
|
],
|
|
),
|
|
if (!failed)
|
|
generateSection(
|
|
title: localizations.network,
|
|
items: _networkRows(context, quality),
|
|
),
|
|
if (failed && error is IpQualityLookupException)
|
|
generateSection(
|
|
title: localizations.ipQualitySources,
|
|
items: [
|
|
for (final failure in error.failures)
|
|
DetailRow(
|
|
title: failure.source.label,
|
|
value: Text(failure.status.label(context)),
|
|
),
|
|
],
|
|
),
|
|
generateSection(
|
|
title: localizations.other,
|
|
items: const [_HideIpItem()],
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
/// A lookup still running lays out every row it will fill, so the page
|
|
/// keeps its height when the values arrive.
|
|
List<Widget> _qualityRows(BuildContext context, IpQuality? quality) {
|
|
final localizations = context.appLocalizations;
|
|
if (quality == null) {
|
|
return [
|
|
DetailRow(
|
|
title: localizations.ipQualityLevel,
|
|
value: const SkeletonText(width: 40),
|
|
),
|
|
DetailRow(
|
|
title: localizations.ipType,
|
|
value: const SkeletonText(width: 64),
|
|
),
|
|
DetailRow(
|
|
title: localizations.ipFlags,
|
|
value: const SkeletonText(width: 40),
|
|
),
|
|
];
|
|
}
|
|
final flags = [
|
|
if (quality.isTor) localizations.ipFlagTor,
|
|
if (quality.isAbuser) localizations.ipFlagAbuser,
|
|
if (quality.isVpn) localizations.ipFlagVpn,
|
|
if (quality.isProxy) localizations.ipFlagProxy,
|
|
];
|
|
return [
|
|
DetailRow(
|
|
title: localizations.ipQualityLevel,
|
|
value: Text(
|
|
quality.level.label(context),
|
|
style: TextStyle(color: quality.level.color(context)),
|
|
),
|
|
),
|
|
DetailRow(
|
|
title: localizations.ipType,
|
|
value: Text(quality.type.label(context)),
|
|
),
|
|
DetailRow(
|
|
title: localizations.ipFlags,
|
|
value: flags.isEmpty
|
|
? Text(localizations.none)
|
|
: Wrap(
|
|
spacing: 6,
|
|
runSpacing: 4,
|
|
alignment: WrapAlignment.end,
|
|
children: [for (final flag in flags) MetaChip(label: flag)],
|
|
),
|
|
),
|
|
];
|
|
}
|
|
|
|
List<Widget> _networkRows(BuildContext context, IpQuality? quality) {
|
|
final localizations = context.appLocalizations;
|
|
Widget valueOf(String? text, {required double width}) =>
|
|
switch ((quality, text)) {
|
|
(null, _) => SkeletonText(width: width),
|
|
(_, final text?) => Text(text),
|
|
_ => const Text('—'),
|
|
};
|
|
final organization = quality?.organization;
|
|
final asn = switch (quality?.asn) {
|
|
final asn? => 'AS$asn',
|
|
null => null,
|
|
};
|
|
return [
|
|
DetailRow(
|
|
title: localizations.ipOrganization,
|
|
copyText: organization,
|
|
value: valueOf(organization, width: 120),
|
|
),
|
|
DetailRow(
|
|
title: localizations.ipAsn,
|
|
copyText: asn,
|
|
value: valueOf(asn, width: 56),
|
|
),
|
|
DetailRow(
|
|
title: localizations.ipQualitySource,
|
|
value: valueOf(quality?.source.label, width: 72),
|
|
),
|
|
];
|
|
}
|
|
}
|